Python中继承的意义是什么?

86 投票
11 回答
23275 浏览
提问于 2025-04-15 12:23

假设你遇到了以下情况

#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那样)。不过我并不是说这更好,只是另一种方式。

我还发现了一个类似的帖子

11 个回答

12

在Python中,继承更多的是一种方便的功能,而不是必须的东西。我觉得它最好的用法是给一个类提供“默认行为”。

实际上,有很多Python开发者认为根本不应该使用继承。无论你怎么做,都不要过度使用。类的层级结构如果太复杂,就很容易被人称为“Java程序员”,这可不是你想要的。:-)

13

在Python中,继承就是为了重复利用代码。你可以把一些共同的功能放到一个基础类里,然后在派生类中实现不同的功能。

81

你提到的运行时鸭子类型被称为“重写”继承,但我认为继承作为一种设计和实现的方法,有它自己的优点,它是面向对象设计中不可或缺的一部分。在我看来,能否用其他方式实现某个功能并不是很重要,因为实际上你可以在Python中不使用类、函数等来编程,但问题在于你的代码设计得有多好,是否健壮,是否易读。

我可以举两个例子,说明在我看来继承是正确的做法,我相信还有更多的例子。

首先,如果你编程得当,你的makeSpeak函数可能想要验证它的输入确实是一个动物,而不仅仅是“它会说话”。在这种情况下,最优雅的方法就是使用继承。你当然可以用其他方式实现,但这就是面向对象设计和继承的美妙之处——你的代码会“真正”检查输入是否是“动物”。

第二个例子,更加简单明了,就是封装——这是面向对象设计的另一个重要部分。当父类有数据成员和/或非抽象方法时,这一点就变得重要了。举个简单的例子,假设父类有一个函数(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"

假设是一个重要的功能,你不想在Dog和Cat中都写一遍,我相信你能从这个例子中推导出更多的情况。当然,你可以实现一个Python独立函数,接受某个鸭子类型的对象,检查它是否有一个说话的函数并调用两次,但这既不优雅,也错过了第一点(验证它是动物)。更糟糕的是,为了加强封装的例子,如果子类中的一个成员函数想要使用呢?

如果父类有一个数据成员,比如,并且这个成员在父类的非抽象方法中使用,比如,但它是在子类的构造函数中初始化的(例如,Dog会用4初始化,而Snake会用0初始化),那么情况就更加清晰了。

再说一次,我相信还有无数更多的例子,但基本上,每个基于扎实的面向对象设计的大型软件都需要继承。

撰写回答