python - Python中的继承有什么意义?

标签 python oop inheritance

假设你有以下情况

#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 接口(interface)非常相似,因为它只包含一个纯虚方法。 makeSpeak 不知道它通过的 Animal 的性质。它只是向它发送信号“speak”,并让后期绑定(bind)处理调用哪个方法: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)

现在,在此示例中,您会看到相同的策略。您使用继承来利用 Dogs 和 Cats 都是 Animals 的分层概念。 但在 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 的情况下,它会在运行时引发异常,这甚至可能是预期的行为。

另一方面,假设您添加了一个带有方法 speak() 的 MouthOfTruth 类。在 C++ 案例中,您必须重构层次结构,或者必须定义不同的 makeSpeak 方法来接受 MouthOfTruth 对象,或者在 java 中,您可以将行为提取到 CanSpeakIface 并为每个对象实现接口(interface)。有很多解决方案...

我想指出的是,我还没有找到在 Python 中使用继承的单一理由(除了框架和异常树,但我猜还有替代策略)。您无需实现基派生层次结构即可以多态方式执行。如果您想使用继承来重用实现,您可以通过包含和委托(delegate)来完成相同的操作,另外还有一个好处是您可以在运行时更改它,并且您可以清楚地定义包含的接口(interface),而不会冒意外副作用的风险。

那么,最后的问题是:在 Python 中继承有什么意义?

编辑:感谢您提供非常有趣的答案。事实上,你可以将它用于代码重用,但我在重用实现时总是很小心。一般来说,我倾向于做非常浅的继承树或根本不做树,如果一个功能很常见,我将它重构为一个公共(public)模块例程,然后从每个对象中调用它。我确实看到了单点更改的好处(例如,我只添加到 Animal,而不是添加到 Dog、Cat、Moose 等,这是继承的基本优势),但是您可以通过一个委托(delegate)链(例如,一个 JavaScript)。我并不是说它更好,只是另一种方式。

我还找到了 a similar post在这方面。

最佳答案

您将运行时鸭子类型称为“覆盖”继承,但是我相信继承作为一种设计和实现方法有其自身的优点,它是面向对象设计的一个组成部分。在我看来,你是否能以其他方式实现某些东西的问题并不是很相关,因为实际上你可以在没有类、函数等的情况下编写 Python 代码,但问题是你的代码的设计、健壮和可读性如何。

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

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

其次,显然更直接的是封装——面向对象设计的另一个组成部分。当祖先具有数据成员和/或非抽象方法时,这变得相关。举一个愚蠢的例子,其中祖先有一个函数(speak_twice),它调用了一个 then-abstract 函数:

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 独立函数,该函数将接受一些鸭子类型的对象,检查它是否有一个 speak 函数并调用它两次,但这既不优雅,也错过了第 1 点(验证它是一个动物)。更糟糕的是,为了加强封装示例,如果后代类中的成员函数要使用 "speak_twice" 怎么办?

如果祖先类有一个数据成员,例如 "number_of_legs" 被祖先中的非抽象方法使用,例如 "print_number_of_legs",那就更清楚了, 但在后代类的构造函数中启动(例如,Dog 会用 4 初始化它,而 Snake 会用 0 初始化它)。

同样,我相信还有无穷无尽的示例,但基本上每个(足够大的)基于可靠的面向对象设计的软件都需要继承。

关于python - Python中的继承有什么意义?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1020453/

相关文章:

python - 在 python 中转义 bash 命令的最佳方法是什么?

python - 如何模拟单例类方法

javascript - JS, "objects",这个那个。来自 python 这真的让我感到困惑。 (双关语)

c# - 反射或动态调度

c++ - 在派生类中使用指令将继承更改为 Public

python - 尝试从其他应用程序导入模型时出现 Django 导入错误

python - 查找网站上有 soup.findall unicode 问题的页面数

php - 多重继承支持

c++ - 为了确保 C++ 类型是 POD,我必须遵循哪些规则?

c++ - 防止覆盖和/或隐藏基类函数 (C++ 11)