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