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

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

#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. Обратите внимание, что я стараюсь быть как можно более похожим на случай С++ на мгновение:

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. В случае С++ он не будет компилироваться, так как Airplane не является производным классом Animal. В случае Python он будет создавать исключение во время выполнения, что может даже быть ожидаемым поведением.

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

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

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

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

Я также нашел подобное сообщение в этом отношении.

Ответ 1

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

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

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

Во-вторых, и, очевидно, более простой, является инкапсуляция - еще одна неотъемлемая часть объектно-ориентированного дизайна. Это становится актуальным, когда у предка есть члены данных и/или не абстрактные методы. Возьмите следующий глупый пример, в котором у предка есть функция (talk_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, тогда как Snake будет инициализировать его с помощью 0).

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

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

Скажите, что у вас есть d, который является Собакой, которая подклассифицировала Animal.

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

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

Используя это, вы можете создать любую комбинацию гибридного монстра Mammal/Reptile/Bird, которую вы хотите, и теперь вы можете сказать "Bark!". во время полета и высунув свой раздвоенный язык, и он справится с этим правильно! Получайте удовольствие от этого!

Ответ 9

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

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

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

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

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

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

В лагере наследования есть аргумент, который страдают реализации чистого делегирования, потому что они требуют много методов "клея", которые просто передают значения через цепочку делегирования. Тем не менее, это просто переосмыслить дизайн, подобный наследованию, с помощью делегирования. Программисты с слишком многолетним опытом работы с наследованием особенно уязвимы для попадания в эту ловушку, так как, не осознавая этого, они будут думать о том, как они будут реализовывать что-то, используя наследование, а затем конвертировать это в делецию.

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

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

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

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

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

Ответ 10

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

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

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

Ответ 11

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

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

from world.animals import Dog

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

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