В чем смысл наследования в Python?

83

Предположим, у вас есть следующая ситуация

#include <iostream>

class Animal {
public:
    virtual void speak() = 0;
};

class Dog : public Animal {
    void speak() { std::cout << "woff!" <<std::endl; }
};

class Cat : public Animal {
    void speak() { std::cout << "meow!" <<std::endl; }
};

void makeSpeak(Animal &a) {
    a.speak();
}

int main() {
    Dog d;
    Cat c;
    makeSpeak(d);
    makeSpeak(c);
}

Как видите, makeSpeak - это процедура, которая принимает общий объект Animal. В этом случае Animal очень похож на интерфейс Java, поскольку он содержит только чистый виртуальный метод. makeSpeak не знает природу объекта Animal, которому он передается. Он просто посылает ему сигнал «говорить» и оставляет позднее связывание, чтобы позаботиться о том, какой метод вызывать: либо Cat :: Speak (), либо Dog :: Speak (). Это означает, что в отношении makeSpeak знание того, какой подкласс фактически передается, не имеет значения.

Но как насчет Python? Давайте посмотрим на код для того же случая в Python. Обратите внимание, что я стараюсь на мгновение быть как можно более похожим на случай C ++:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

Теперь в этом примере вы видите ту же стратегию. Вы используете наследование, чтобы усилить иерархическую концепцию, согласно которой и собаки, и кошки являются животными. Но в Python эта иерархия не нужна. Это работает одинаково хорошо

class Dog:
    def speak(self):
        print "woff!"

class Cat:
    def speak(self):
        print "meow"

def makeSpeak(a):
    a.speak()

d=Dog()
c=Cat()
makeSpeak(d)
makeSpeak(c)

В Python вы можете послать сигнал «говорить» любому объекту, который хотите. Если объект может с этим справиться, он будет выполнен, в противном случае он вызовет исключение. Предположим, вы добавляете класс Airplane в оба кода и отправляете объект Airplane в makeSpeak. В случае C ++ он не будет компилироваться, поскольку Airplane не является производным классом от Animal. В случае Python он вызовет исключение во время выполнения, что может быть даже ожидаемым поведением.

С другой стороны, предположим, что вы добавляете класс MouthOfTruth с методом speak (). В случае C ++ вам либо придется реорганизовать иерархию, либо вам придется определить другой метод makeSpeak для приема объектов MouthOfTruth, либо в java вы можете извлечь поведение в CanSpeakIface и реализовать интерфейс для каждого из них. Есть много решений ...

Я хотел бы отметить, что я еще не нашел ни одной причины для использования наследования в Python (кроме фреймворков и деревьев исключений, но я предполагаю, что существуют альтернативные стратегии). вам не нужно реализовывать базовую иерархию для полиморфного выполнения. Если вы хотите использовать наследование для повторного использования реализации, вы можете сделать то же самое с помощью включения и делегирования, с дополнительным преимуществом, которое вы можете изменить во время выполнения, и вы четко определяете интерфейс вложенного, без риска непредвиденных побочных эффектов.

Итак, в конце концов, стоит вопрос: в чем смысл наследования в Python?

Изменить : спасибо за очень интересные ответы. Действительно, вы можете использовать его для повторного использования кода, но я всегда осторожен при повторном использовании реализации. В общем, я обычно делаю очень мелкие деревья наследования или вообще не использую дерево, и если функциональность является общей, я реорганизую ее как обычную модульную процедуру, а затем вызываю ее из каждого объекта. Я действительно вижу преимущество наличия одной единственной точки изменения (например, вместо добавления к Dog, Cat, Moose и т. Д. Я просто добавляю к Animal, что является основным преимуществом наследования), но вы можете добиться того же с помощью цепочка делегирования (например, а-ля JavaScript). Я не утверждаю, что это лучше, просто другой способ.

Я также нашел похожий пост по этому поводу.

Стефано Борини
источник
18
-1: «вы можете добиться того же с помощью цепочки делегирования». Верно, но гораздо болезненнее, чем наследование. Вы можете добиться того же, вообще не используя никаких определений классов, просто используя множество сложных чистых функций. Вы можете достичь того же самым десятком способов, и все они менее просты, чем наследование.
S.Lott
10
на самом деле я сказал: «Я не утверждаю, что это лучше;)»
Стефано Борини
4
«Я еще не нашел ни одной причины использовать наследование в python» ... конечно, звучит как «мое решение лучше».
S.Lott
10
Извините, если это произвело на вас такое впечатление. Мой пост был направлен на получение положительных отзывов о реальных примерах использования наследования в Python, которые на сегодняшний день я не смог найти (в основном потому, что во всех моих программах на Python я сталкивался со случаями, когда в этом не было необходимости, и когда Я сделал, это была ситуация, которую я объяснил выше).
Стефано Борини
2
Таксономии реального мира редко служат хорошей основой для примеров объектной ориентации.
Apalala

Ответы:

81

Вы имеете в виду утиную типизацию во время выполнения как «переопределяющее» наследование, однако я считаю, что у наследования есть свои достоинства как подход к дизайну и реализации, поскольку он является неотъемлемой частью объектно-ориентированного дизайна. По моему скромному мнению, вопрос о том, сможете ли вы добиться чего-то в противном случае, не очень актуален, потому что на самом деле вы можете кодировать Python без классов, функций и многого другого, но вопрос в том, насколько хорошо продуманным, надежным и читаемым будет ваш код.

Я могу привести два примера, где, на мой взгляд, правильным подходом является наследование, я уверен, что их гораздо больше.

Во-первых, если вы кодируете с умом, ваша функция makeSpeak может захотеть проверить, что ее ввод действительно является Animal, а не только тем, что «он может говорить», и в этом случае наиболее элегантным методом будет использование наследования. Опять же, вы можете сделать это другими способами, но в этом прелесть объектно-ориентированного дизайна с наследованием - ваш код «действительно» проверит, является ли ввод «животным».

Во-вторых, что гораздо проще, это инкапсуляция - еще одна неотъемлемая часть объектно-ориентированного дизайна. Это становится актуальным, когда у предка есть элементы данных и / или неабстрактные методы. Возьмите следующий глупый пример, в котором у предка есть функция (speak_twice), которая вызывает тогда абстрактную функцию:

class Animal(object):
    def speak(self):
        raise NotImplementedError()

    def speak_twice(self):
        self.speak()
        self.speak()

class Dog(Animal):
    def speak(self):
        print "woff!"

class Cat(Animal):
    def speak(self):
        print "meow"

Предполагая, "speak_twice"что это важная функция, вы не хотите кодировать ее как в Dog, так и в Cat, и я уверен, что вы можете экстраполировать этот пример. Конечно, вы могли бы реализовать автономную функцию Python, которая будет принимать какой-нибудь объект типа утки, проверять, есть ли у него функция Speech, и вызывать ее дважды, но это не элегантно и пропускает пункт номер 1 (подтвердите, что это животное). Что еще хуже, и чтобы усилить пример инкапсуляции, что, если функция-член в классе-потомке захотела бы использовать "speak_twice"?

Это становится еще яснее, если класс-предок имеет член данных, например, "number_of_legs"который используется неабстрактными методами в предке, например "print_number_of_legs", но инициируется в конструкторе класса-потомка (например, Dog инициализирует его с помощью 4, тогда как Snake инициализирует его с 0).

Опять же, я уверен, что существует бесконечное количество примеров, но в основном каждое (достаточно большое) программное обеспечение, основанное на твердом объектно-ориентированном дизайне, потребует наследования.

Рои Адлер
источник
3
В первом случае это будет означать, что вы проверяете типы, а не поведение, что в некотором роде неприемлемо. Во втором случае я согласен, и вы в основном придерживаетесь «рамочного» подхода. Вы перерабатываете реализацию speak_twice, не только интерфейс, но и для переопределения, вы можете жить без наследования, когда рассматриваете python.
Стефано Борини
9
Вы можете жить без многих вещей, таких как классы и функции, но вопрос в том, что делает код прекрасным. Я думаю, наследование имеет значение.
Рои Адлер
@Stefano Borini - Похоже, вы придерживаетесь подхода, основанного на правилах. Но старое клише верно: они созданы для того, чтобы их ломали. :-)
Джейсон Бейкер
@ Джейсон Бейкер - Мне нравятся правила, потому что они сообщают о мудрости, полученной из опыта (например, об ошибках), но мне не нравится, когда они препятствуют творчеству. Так что я согласен с вашим утверждением.
Стефано Борини
1
Я не нахожу этот пример таким ясным - животные, автомобили и примеры форм действительно отстой для этих обсуждений :) Единственное, что имеет значение, IMHO, - хотите ли вы наследовать реализацию или нет. Если так, правила в Python действительно похожи на java / C ++; разница в основном касается наследования интерфейсов. В этом случае часто выходом оказывается утиная типизация - гораздо больше, чем наследование.
Дэвид Курнапо,
12

Наследование в Python - это повторное использование кода. Факторизуйте общие функции в базовом классе и реализуйте различные функции в производных классах.

Роберто Бонвале
источник
11

Наследование в Python удобнее, чем что-либо еще. Я считаю, что лучше всего использовать его, чтобы предоставить классу «поведение по умолчанию».

Действительно, существует значительное сообщество разработчиков Python, которые вообще выступают против использования наследования. Что бы вы ни делали, не переусердствуйте. Слишком сложная иерархия классов - верный способ получить ярлык «Java-программиста», а у вас просто не может быть этого. :-)

Джейсон Бейкер
источник
8

Я думаю, что цель наследования в Python заключается не в том, чтобы компилировать код, а в реальной причине наследования, которое расширяет класс до другого дочернего класса и переопределяет логику в базовом классе. Однако «утка» в Python делает концепцию «интерфейса» бесполезной, потому что вы можете просто проверить, существует ли метод перед вызовом, без необходимости использовать интерфейс для ограничения структуры класса.

Башмоханды
источник
3
Селективное переопределение является причиной наследования. Если вы собираетесь переопределить все, это особый случай.
S.Lott
1
Кто все отвергнет? вы можете думать о python так, как будто все методы общедоступны и виртуальны
bashmohandes
1
@bashmohandes: Я бы никогда не стал все отменять. Но вопрос показывает вырожденный случай, когда все отменяется; этот странный частный случай является основанием для вопроса. Поскольку в обычном объектно-ориентированном дизайне этого никогда не происходит, вопрос бессмысленный.
S.Lott
7

Я думаю, что на таких абстрактных примерах очень сложно дать осмысленный, конкретный ответ ...

Для упрощения существует два типа наследования: интерфейс и реализация. Если вам нужно унаследовать реализацию, тогда python не так уж отличается от статически типизированных объектно-ориентированных языков, таких как C ++.

Наследование интерфейса - вот где есть большая разница, с фундаментальными последствиями для дизайна вашего программного обеспечения, по моему опыту. Такие языки, как Python, не заставляют вас использовать наследование в этом случае, и в большинстве случаев полезно избегать наследования, потому что позже очень сложно исправить неправильный выбор дизайна. Это хорошо известный вопрос, который поднимается в любой хорошей книге по ООП.

Есть случаи, когда в Python рекомендуется использовать наследование для интерфейсов, например, для плагинов и т. Д. Для этих случаев в Python 2.5 и ниже отсутствует «встроенный» элегантный подход, и несколько крупных фреймворков разработали собственные решения (zope, trac, twister). Python 2.6 и выше имеет классы ABC для решения этой проблемы .

Дэвид Курнапо
источник
6

Утиная типизация делает бессмысленным не наследование, а интерфейсы - вроде того, который вы выбрали при создании полностью абстрактного класса животных.

Если бы вы использовали класс животных, который вводит какое-то реальное поведение для его потомков, тогда классы собак и кошек, которые вводят какое-то дополнительное поведение, были бы причиной для обоих классов. Ваш аргумент верен только в случае, если класс-предок не вносит фактический код в классы-потомки.

Поскольку Python может напрямую знать возможности любого объекта, и поскольку эти возможности изменяются за пределами определения класса, идея использования чистого абстрактного интерфейса для «указания» программе, какие методы могут быть вызваны, в некоторой степени бессмысленна. Но это не единственный и даже не главный пункт наследования.

Ларри Люстиг
источник
5

В C ++ / Java / и т. Д. Полиморфизм вызван наследованием. Откажитесь от этого ошибочного убеждения, и вам откроются динамические языки.

По сути, в Python нет столько интерфейса, сколько «понимания того, что определенные методы могут быть вызваны». Довольно волнистые и академически звучащие, не так ли? Это означает, что, поскольку вы вызываете "говорить", вы явно ожидаете, что у объекта должен быть метод "говорить". Просто, а? Это очень по-лисковски, поскольку пользователи класса определяют его интерфейс, хорошая концепция дизайна, которая приводит вас к более здоровому TDD.

Так что остается, как вежливо избегал другой автор, трюк с совместным использованием кода. Вы можете записать одно и то же поведение в каждый «дочерний» класс, но это будет лишним. Легче наследовать или смешивать функциональность, неизменную в иерархии наследования. Меньший, DRY-код в целом лучше.

проворный
источник
2

Не вижу особого смысла в наследовании.

Каждый раз, когда я когда-либо использовал наследование в реальных системах, я горел, потому что это приводило к запутанной паутине зависимостей, или я просто вовремя понимал, что мне было бы намного лучше без него. Теперь я избегаю этого по мере возможности. Мне это просто никогда не пригодится.

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

Джеймсу Гослингу однажды задали на пресс-конференции вопрос примерно следующего содержания: «Если бы вы могли вернуться и сделать Java по-другому, что бы вы оставили?». Его ответ был «Классы», над которыми был смех. Однако он был серьезен и объяснил, что на самом деле проблема заключалась не в классах, а в наследовании.

Я рассматриваю это как наркотическую зависимость - это дает вам быстрое решение, которое кажется хорошим, но в конце концов сбивает вас с толку. Под этим я подразумеваю, что это удобный способ повторного использования кода, но он вызывает нездоровую связь между дочерним и родительским классами. Изменения родителя могут сломать ребенка. Дочерний элемент зависит от родителя в отношении определенных функций и не может изменять эти функции. Поэтому функциональность, предоставляемая дочерним элементом, также связана с родительским - у вас может быть только и то, и другое.

Лучше предоставить один единственный клиентский класс для интерфейса, который реализует интерфейс, используя функциональность других объектов, которые создаются во время создания. Делая это через правильно спроектированные интерфейсы, можно исключить все связи, и мы предоставляем легко компонуемый API (в этом нет ничего нового - большинство программистов уже это делают, но этого недостаточно). Обратите внимание, что реализующий класс не должен просто предоставлять функциональные возможности, в противном случае клиент должен просто напрямую использовать составленные классы - он должен делать что-то новое, комбинируя эти функциональные возможности.

Сторонники наследования утверждают, что реализация чистого делегирования страдает, потому что для этого требуется множество «клеящих» методов, которые просто передают значения через «цепочку» делегирования. Однако это просто переосмысление конструкции, похожей на наследование, с использованием делегирования. Программисты, которые слишком много лет знакомы с проектами, основанными на наследовании, особенно уязвимы для попадания в эту ловушку, поскольку, не осознавая этого, они будут думать о том, как бы реализовать что-то с использованием наследования, а затем преобразовать это в делегирование.

Правильное разделение проблем, таких как приведенный выше код, не требует склеивающих методов, поскольку каждый шаг фактически добавляет ценность , поэтому они вообще не являются «склеивающими» методами (если они не добавляют ценности, дизайн ошибочен).

Все сводится к следующему:

  • Для повторно используемого кода каждый класс должен делать только одно (и делать это хорошо).

  • Наследование создает классы, которые выполняют несколько функций, потому что они смешаны с родительскими классами.

  • Следовательно, использование наследования делает классы трудными для повторного использования.

Майк А
источник
1

Вы можете обойтись без наследования в Python и почти на любом другом языке. Однако все дело в повторном использовании кода и упрощении кода.

Просто семантический трюк, но после создания ваших классов и базовых классов вам даже не нужно знать, что возможно с вашим объектом, чтобы увидеть, сможете ли вы это сделать.

Скажем, у вас есть d - собака, являющаяся подклассом Animal.

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

Если все, что ввел пользователь, доступно, код запустит правильный метод.

Используя это, вы можете создать любую комбинацию гибридного чудовища млекопитающих / рептилий / птиц, которое вы хотите, и теперь вы можете заставить его говорить «Кора!» во время полета и высунув свой раздвоенный язык, и он справится с этим правильно! Удачи!

мандроид
источник
1

Еще один небольшой момент - это третий пример op, вы не можете вызвать isinstance (). Например, передача вашего 3-го примера другому объекту, который принимает и набирает "Animal", вызовы говорят на нем. Если вы этого не сделаете, вам придется проверять тип собаки, тип кошки и так далее. Не уверен, что проверка экземпляра действительно "Pythonic", из-за позднего связывания. Но тогда вам придется реализовать какой-то способ, которым AnimalControl не пытается бросать типы чизбургеров в грузовик, потому что чизбургеры не разговаривают.

class AnimalControl(object):
    def __init__(self):
        self._animalsInTruck=[]

    def catachAnimal(self,animal):
        if isinstance(animal,Animal):
            animal.speak()  #It's upset so it speak's/maybe it should be makesNoise
            if not self._animalsInTruck.count <=10:
                self._animalsInTruck.append(animal) #It's then put in the truck.
            else:
                #make note of location, catch you later...
        else:
            return animal #It's not an Animal() type / maybe return False/0/"message"
yedevtxt
источник
0

Классы в Python - это просто способы группировки множества функций и данных. Они отличаются от классов в C ++ и т. Д.

В основном я видел наследование, используемое для переопределения методов суперкласса. Например, возможно, более питонское использование наследования было бы ..

from world.animals import Dog

class Cat(Dog):
    def speak(self):
        print "meow"

Конечно , кошки не тип собаки, но у меня есть это (третья сторона) Dogкласс , который работает отлично, за исключением того, в speakметоде , который я хочу , чтобы переопределить - это экономит повторное внедрение весь класс, просто так мяукает. Опять же, while Cat- это не тип Dog, но кошка наследует множество атрибутов ..

Намного лучший (практический) пример переопределения метода или атрибута - это изменение пользовательского агента для urllib. Вы в основном подклассифицируете urllib.FancyURLopenerи меняете атрибут версии ( из документации ):

import urllib

class AppURLopener(urllib.FancyURLopener):
    version = "App/1.7"

urllib._urlopener = AppURLopener()

Другой способ использования исключений - для исключений, когда наследование используется более "правильным" способом:

class AnimalError(Exception):
    pass

class AnimalBrokenLegError(AnimalError):
    pass

class AnimalSickError(AnimalError):
    pass

..вы можете поймать AnimalErrorвсе исключения, которые наследуются от него, или конкретное, например AnimalBrokenLegError

dbr
источник
6
Я ... немного сбит с толку вашим первым примером. Последнее, что я проверял, кошки - это не собаки, поэтому я не уверен, какие отношения вы пытаетесь продемонстрировать. :-)
Бен Бланк
1
Вы нарушаете принцип Лискова: кошка - это НЕ собака. В этом случае можно использовать, но что, если класс Dog изменится и получит, например, поле «Lead», что для кошек бессмысленно?
Дмитрий Рисенберг,
1
Что ж, если нет базового класса Animal, ваша альтернатива - заново реализовать все это ... Я не говорю, что это лучшая практика (если есть базовый класс Animal, используйте его), но он работает и используется обычно ( это рекомендуемый способ изменения пользовательского агента urllib, согласно добавленному мною примеру)
dbr