В чем смысл наследования в 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 очень похож на интерфейс Java, поскольку он содержит только чистый виртуальный метод. makeSpeak не знает природу животного, которое он проходит. Он просто посылает ему сигнал "говорить" и оставляет позднюю привязку, чтобы позаботиться о том, какой метод вызвать: либо Cat::speak (), либо Dog::speak(). Это означает, что, насколько makeSpeak обеспокоен, знание того, какой подкласс фактически передается, не имеет значения.
а как же питон? Давайте посмотрим код для того же случая в 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 вы можете отправить сигнал "говорить" на любой объект, который вы хотите. Если объект способен справиться с этим, он будет выполнен, иначе он вызовет исключение. Предположим, что вы добавляете самолета класса в оба кодекса, и представить объект самолет makeSpeak. В случае C++ он не будет компилироваться, так как самолет не является производным классом животного. В случае Python это вызовет исключение во время выполнения, которое может даже быть ожидаемым поведением.
на с другой стороны, предположим, что вы добавляете класс MouthOfTruth с помощью метода speak(). В случае C++ вам придется либо рефакторировать свою иерархию, либо определить другой метод makeSpeak для принятия объектов MouthOfTruth, либо в java вы можете извлечь поведение в CanSpeakIface и реализовать интерфейс для каждого. Есть много решений...
Я хотел бы отметить, что я еще не нашел ни одной причины использовать наследование в Python (кроме фреймворков и деревья исключений, но я думаю, что альтернативные стратегии существуют). вам не нужно реализовывать базовую иерархию для выполнения полиморфно. Если вы хотите использовать наследование для повторного использования реализации, вы можете сделать то же самое через сдерживание и делегирование, с дополнительным преимуществом, что вы можете изменить его во время выполнения, и вы четко определяете интерфейс содержащихся, не рискуя непреднамеренными побочными эффектами.
Итак, в конце концов, встает вопрос: в чем смысл наследование в Python?
Edit: спасибо за очень интересные ответы. Действительно, Вы можете использовать его для повторного использования кода, но я всегда осторожен при повторном использовании реализации. В общем, я обычно делаю очень мелкие деревья наследования или вообще не дерево, и если функциональность является общей, Я рефакторирую ее как общую процедуру модуля, а затем вызываю ее из каждого объекта. Я вижу преимущество наличия одной единственной точки изменения (например. вместо добавления к собаке, кошке, Лосю и так далее я просто добавляю к животному, что является основным преимуществом наследования), но вы можете добиться того же с цепочкой делегирования (например. а-ля JavaScript). Я не утверждаю, что это лучше, просто другой способ.
Я нашел аналогичной должности по этому поводу.
11 ответов:
вы имеете в виду утку времени выполнения как "переопределяющее" наследование, однако я считаю, что наследование имеет свои собственные достоинства как подход к проектированию и реализации, являясь неотъемлемой частью объектно-ориентированного дизайна. По моему скромному мнению, вопрос о том, Можете ли вы достичь чего-то другого, не очень актуален, потому что на самом деле вы можете кодировать Python без классов, функций и т. д., Но вопрос в том, насколько хорошо спроектированный, надежный и читаемый ваш код будет быть.
Я могу привести два примера, где наследование является правильным подходом, на мой взгляд, я уверен, что их больше.
во - вторых, и явно более простой, является инкапсуляция-еще одна неотъемлемая часть объектно-ориентированного дизайна. Это становится актуальным, когда предок имеет элементы данных и/или неабстрактные методы. Возьмем следующий глупый пример, в котором предок имеет функцию (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"является важной особенностью, вы не хотите, чтобы кодировать его в собаке и кошке, и я уверен, что вы можно экстраполировать этот пример. Конечно, вы можете реализовать автономную функцию Python, которая будет принимать какой-то объект типа duck, проверять, есть ли у него функция speak и вызывать ее дважды, но это не элегантно и пропускает точку № 1 (проверьте, что это животное). Еще хуже, и для усиления примера инкапсуляции, что делать, если функция-член в классе-потомке хотела использовать"speak_twice"?становится еще яснее, если класс предка имеет член данных, например
"number_of_legs"это используется неабстрактными методами в предке, как"print_number_of_legs", но инициализируется в конструкторе класса-потомка (например, Dog инициализирует его с помощью 4, тогда как Snake инициализирует его с помощью 0).опять же, я уверен, что есть бесконечное количество примеров, но в основном каждое (достаточно большое) программное обеспечение, основанное на твердом объектно-ориентированном дизайне, потребует наследования.
наследование в Python - это повторное использование кода. Разложим общую функциональность в базовый класс и реализовать различные функции в производных классах.
наследование в Python является более удобным, чем что-либо еще. Я считаю, что лучше всего использовать для предоставления класса с "поведением по умолчанию."
действительно, существует значительное сообщество разработчиков Python, которые возражают против использования наследования вообще. Что бы вы ни делали, не переусердствуйте. Наличие слишком сложной иерархии классов-это верный способ получить ярлык "Java-программиста", и вы просто не можете этого сделать. : -)
Я думаю, что точка наследования в Python-это не компиляция кода, а реальная причина наследования, которая расширяет класс в другой дочерний класс и переопределяет логику в базовом классе. Однако утка, набранная в Python, делает концепцию" интерфейса " бесполезной, потому что вы можете просто проверить, существует ли метод перед вызовом без необходимости использовать интерфейс для ограничения структуры класса.
Я думаю, что очень трудно дать осмысленный, конкретный ответ с такими абстрактными примерами...
для упрощения существует два типа наследования: интерфейс и реализация. Если вам нужно наследовать реализацию, то python не так уж отличается от статически типизированных языков OO, таких как C++.
наследование интерфейса, где есть большая разница, с фундаментальными последствиями для дизайна вашего программного обеспечения в моем опыте. Языки как Python не заставляет вас использовать наследование в этом случае, и избежать наследования является хорошим моментом в большинстве случаев, потому что очень трудно исправить неправильный выбор дизайна там позже. Это хорошо известный момент, поднятый в любой хорошей книге ООП.
есть случаи, когда использование наследования для интерфейсов целесообразно в Python, например для плагинов и т. д... Для этих случаев Python 2.5 и ниже не хватает" встроенного " элегантного подхода, и несколько больших фреймворков разработали свои собственные решения (zope, trac, twister). Python 2.6 и выше ABC классы, чтобы решить эту.
в C++/Java/etc полиморфизм вызван наследованием. Откажитесь от этой ложной веры, и вам откроются динамические языки.
по сути, в Python нет такого интерфейса, как"понимание того, что некоторые методы могут быть вызваны". Довольно волнистые руки и академическое звучание, нет? Это означает, что поскольку вы вызываете "говорить", вы явно ожидаете, что объект должен иметь метод "говорить". Просто, да? Это очень Лисков-Ян в том, что пользователи класса определяют интерфейс, хорошая концепция дизайна, которая приводит вас к более здоровому TDD.
Итак, что осталось, как вежливо удалось избежать другого плаката, трюк с обменом кодом. Вы могли бы написать такое же поведение в каждом "дочернем" классе, но это было бы излишним. Проще наследовать или смешивать функции, которые инвариантны в иерархии наследования. Меньший, сухой код лучше в целом.
Это не наследование, которое утка-типизация делает бессмысленным, это интерфейсы - как тот, который вы выбрали при создании всего абстрактного класса животных.
Если бы вы использовали класс животных, который вводит некоторое реальное поведение для его потомков, то классы собак и кошек, которые ввели некоторое дополнительное поведение, были бы причиной для обоих классов. Только в случае класса-предка, не вносящего никакого фактического кода в классы-потомки, ваш аргумент правильный.
поскольку Python может непосредственно знать возможности любого объекта, и поскольку эти возможности изменчивы за пределами определения класса, идея использования чистого абстрактного интерфейса, чтобы "сказать" программе, какие методы могут быть вызваны, несколько бессмысленна. Но это не единственный и даже не главный пункт наследования.
вы можете обойти наследование в Python и почти на любом другом языке. Это все о повторном использовании кода и упрощении кода, хотя.
просто семантический трюк, но после создания ваших классов и базовых классов вам даже не нужно знать, что возможно с вашим объектом, чтобы увидеть, можете ли вы это сделать.
скажем, у вас есть d, который является собакой, которая подкласс животное.
command = raw_input("What do you want the dog to do?") if command in dir(d): getattr(d,command)()Если доступно все, что ввел пользователь, код будет работать правильно метод.
С помощью этого вы можете создать любую комбинацию млекопитающих / рептилий / птиц гибридное чудовище, которое вы хотите, и теперь вы можете заставить его сказать " лай!"во время полета и торчит его раздвоенный язык, и он будет обращаться с ним правильно! Получайте удовольствие от этого!
Я не вижу особого смысла в наследство.
каждый раз, когда я когда-либо использовал наследование в реальных системах, я обжегся, потому что это привело к запутанной сети зависимостей, или я просто вовремя понял, что мне было бы намного лучше без него. Теперь я стараюсь избегать этого как можно больше. Я просто никогда не использую его.
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 по-другому, что бы вы сделали оставить?". Его ответом были" занятия", над которыми раздавался смех. Однако он был серьезен и объяснил, что на самом деле проблема заключалась не в классах, а в наследовании.
Я как бы рассматриваю это как наркотическую зависимость - это дает вам быстрое решение, которое чувствует себя хорошо, но в конце концов, это портит вас. Под этим я подразумеваю, что это удобный способ повторного использования кода, но он вызывает нездоровую связь между дочерним и родительским классом. Изменения в родителе могут нарушить работу ребенка. Ребенок зависит от родителя для определенной функциональности и не может изменить эту функциональность. Поэтому функциональность, предоставляемая ребенком, также привязана к родителю - вы можете иметь только то и другое.
лучше предоставить один единственный клиентский класс для интерфейса, который реализует интерфейс, используя функциональность других объектов, которые составлены во время построения. Делая это через правильно конструированные интерфейсы, все соединение можно исключить и мы обеспечиваем сильно composable API (это ничего нового - большинство программистов уже делают это, просто недостаточно). Обратите внимание, что реализующий класс не должен просто предоставлять функциональность, иначе клиент должен просто использовать составленные классы напрямую-it должны сделайте что-то новое, объединив эту функциональность.
есть аргумент из лагеря наследования, что чистые реализации делегирования страдают, потому что они требуют много методов "клея", которые просто передают значения через цепь делегация''. Однако это просто изобретение дизайна, подобного наследованию, с использованием делегирования. Программисты со слишком большим количеством лет работы с проектами на основе наследования особенно уязвимы для попадания в эту ловушку, поскольку, не осознавая этого, они будут думать о том, как они будут реализовывать что-то с помощью наследования, а затем преобразовывать это в делегирование.
правильное разделение проблем, как в приведенном выше коде, не требует методов клея, так как каждый шаг на самом деле добавить значением, поэтому они на самом деле не являются "клеевыми" методами вообще (если они не добавляют ценности, дизайн испорчен).
Это сводится к следующему:
для многоразового кода каждый класс должен делать только одну вещь (и это хорошо).
наследование создает классы, которые делают больше чем одна вещь, потому что они вперемешку с родительскими классами.
поэтому использование наследования делает классы, которые трудно использовать повторно.
еще один маленький момент заключается в том, что 3-й пример op, вы не можете вызвать isinstance(). Например, передавая ваш 3-й пример другому объекту, который принимает и" животное " типа вызовов говорят на нем. Если вы этого не сделаете, вам придется проверить тип собаки, тип кошки и так далее. Не уверен, если проверить экземпляр действительно "весть", из-за позднего связывания. Но тогда вам придется реализовать какой-то способ, чтобы 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"
классы в Python в основном просто способы группировки кучу функций и данных.. Они отличаются от классов в C++ и подобных..
я в основном видел наследование, используемое для переопределения методов суперкласса. Например, возможно, более Python'ish использование наследования будет..
from world.animals import Dog class Cat(Dog): def speak(self): print "meow"конечно кошки не тип собаки, но у меня есть это (третья сторона)
Dogкласс, который отлично работает,за исключением thespeakметод, который я хотите переопределить-это сохраняет повторную реализацию всего класса, просто так он мяукает. Опять же, покаCatэто не типDog, но кошка действительно наследует много атрибутов..гораздо лучший (практический) пример переопределения метода или атрибута-это то, как вы меняете user-agent для 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
Comments