Какой смысл наследования в Python? - PullRequest
78 голосов
/ 20 июня 2009

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

#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 не знает природу Животного, которого ему передают. Он просто посылает ему сигнал «говорить» и оставляет позднюю привязку, чтобы позаботиться о том, какой метод вызывать: либо 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). Я не утверждаю, что это лучше, просто по-другому.

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

Ответы [ 11 ]

78 голосов
/ 20 июня 2009

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

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

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

Вторым, и, несомненно, более простым, является Encapsulation - еще одна неотъемлемая часть объектно-ориентированного дизайна. Это становится актуальным, когда у предка есть члены-данные и / или неабстрактные методы. Возьмите следующий глупый пример, в котором у предка есть функция (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, которая будет принимать некоторый объект типа «утка», проверять, есть ли у него функция выступления, и дважды вызывать его, но это не элегантно и не соответствует точке № 1 (проверьте, что это Animal). Что еще хуже, и для усиления примера Encapsulation, что если функция-член в классе-потомке хочет использовать "speak_twice"?

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

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

12 голосов
/ 20 июня 2009

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

10 голосов
/ 20 июня 2009

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

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

8 голосов
/ 20 июня 2009

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

7 голосов
/ 20 июня 2009

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

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

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

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

5 голосов
/ 16 октября 2009

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

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

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

5 голосов
/ 20 июня 2009

В C ++ / Java / etc полиморфизм вызван наследованием. Откажитесь от этой порожденной веры, и динамические языки откроются перед вами.

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

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

1 голос
/ 11 июля 2012

Другим небольшим моментом является то, что в третьем примере этой операции нельзя вызывать 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"
1 голос
/ 02 июля 2009

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

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

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 голос
/ 25 июня 2009

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

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

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...