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

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

77 голосов | спросил Stefano Borini 20 J0000006Europe/Moscow 2009, 03:28:18

11 ответов


0

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

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

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

ответил Roee Adler 20 J0000006Europe/Moscow 2009, 03:35:48
0

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

ответил Roberto Bonvallet 20 J0000006Europe/Moscow 2009, 08:51:10
0

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

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

ответил Jason Baker 20 J0000006Europe/Moscow 2009, 07:44:11
0

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

ответил bashmohandes 20 J0000006Europe/Moscow 2009, 03:34:45
0

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

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

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

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

ответил David Cournapeau 20 J0000006Europe/Moscow 2009, 07:31:25
0

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

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

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

ответил agileotter 20 J0000006Europe/Moscow 2009, 05:01:40
0

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

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

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

ответил Larry Lustig 16 +04002009-10-16T02:28:31+04:00312009bEurope/MoscowFri, 16 Oct 2009 02:28:31 +0400 2009, 02:28:31
0

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

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

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

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

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

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

ответил mandroid 25 J0000006Europe/Moscow 2009, 03:40:17
0

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

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

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

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

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

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

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

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

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

ответил Mike A 2 J000000Thursday09 2009, 00:55:23
0

Еще один маленький момент - третий пример этой операции: нельзя вызывать 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 11 J000000Wednesday12 2012, 18:14:27
0

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

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

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

ответил dbr 20 J0000006Europe/Moscow 2009, 03:52:43

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132