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