Python中的继承点是什么?

Ste*_*ini 80 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);
}
Run Code Online (Sandbox Code Playgroud)

如您所见,makeSpeak是一个接受通用Animal对象的例程.在这种情况下,Animal非常类似于Java接口,因为它只包含一个纯虚方法.makeSpeak不知道它传递的Animal的性质.它只是发送信号"speak"并留下后期绑定来处理调用哪种方法: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)
Run Code Online (Sandbox Code Playgroud)

现在,在此示例中,您将看到相同的策略.您使用继承来利用Dogs和Cats都是动物的分层概念.但在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)
Run Code Online (Sandbox Code Playgroud)

在Python中,您可以将信号"发声"发送到您想要的任何对象.如果对象能够处理它,它将被执行,否则它将引发异常.假设您向两个代码添加了一个类飞机,并将一个Airplane对象提交给makeSpeak.在C++的情况下,它不会编译,因为Airplane不是Animal的派生类.在Python的情况下,它将在运行时引发异常,甚至可能是预期的行为.

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

我想指出的是,我还没有找到在Python中使用继承的一个原因(除了框架和异常树之外,但我想存在替代策略).您不需要实现基于派生的层次结构来执行多态.如果要使用继承来重用实现,可以通过包含和委派来完成相同的操作,还可以在运行时更改它,并清楚地定义包含的接口,而不会产生意外的副作用.

所以,最后,问题在于:Python中的继承点是什么?

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

我也在这方面找到了类似的帖子.

Roe*_*ler 78

您将运行时鸭子类型称为"重写"继承,但我相信继承作为一种设计和实现方法有其自身的优点,是面向对象设计的一个组成部分.在我看来,你是否可以实现某些目标的问题并不是很相关,因为实际上你可以在没有类,函数等的情况下编写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"
Run Code Online (Sandbox Code Playgroud)

假设"speak_twice"是一个重要的功能,你不想在Dog和Cat中编写它,我相信你可以推断这个例子.当然,你可以实现一个Python独立函数,它将接受一些duck-typed对象,检查它是否有一个speak函数并调用它两次,但这都是非优雅的并且错过了第一个点(验证它是一个Animal).更糟糕的是,为了加强Encapsulation示例,如果后代类中的成员函数想要使用该"speak_twice"怎么办?

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

同样,我确信有更多的例子,但基本上每一个(足够大)的软件都基于固体面向对象的设计将需要继承.

  • 你可以在没有很多东西的情况下生活,比如类和函数,但问题是什么使代码变得更好.我认为继承确实如此. (9认同)
  • 赞成第一段的口才 (4认同)
  • 对于第一种情况,这意味着您正在检查类型而不是行为,这是一种unpythonic.对于第二种情况,我同意,你基本上是在做"框架"方法.你正在回收speak_twice的实现,不仅仅是接口,但是对于重写,当你考虑python时,你可以没有继承. (3认同)
  • 我有点不同意您应该使用继承来获取更多属性或功能到派生中。正如原作者提到的,浅层继承和实用程序类/函数而不是将它们放在基类中,从长远来看维护要好得多。当然,为了快速而肮脏,采用现有类、子类化并添加更多功能总是很容易,但它会变得混乱并且是错误的。我已经见过足够多的系统,其中层次结构很深,而且无法遵循。甚至在 Android 代码中看到它,其中在导出中引入了 dup 成员。 (2认同)

Rob*_*let 12

Python中的继承完全是关于代码重用.将常用功能分解为基类,并在派生类中实现不同的功能.


Jas*_*ker 10

Python中的继承比其他任何东西都更方便.我发现它最好用于为类提供"默认行为".

实际上,有一个重要的Python开发者社区反对使用继承.无论你做什么,不要只是不要过度.有一个过于复杂的类层次结构是一个肯定的方式被标记为"Java程序员",你就是不能拥有它.:-)


bas*_*des 8

我认为Python中的继承点不是为了使代码编译,而是为了继承的真正原因,即将类扩展到另一个子类,并覆盖基类中的逻辑.然而,在Python中键入鸭子会使"接口"概念变得毫无用处,因为您可以在调用之前检查方法是否存在,而无需使用接口来限制类结构.

  • 选择性覆盖是继承的原因.如果你要覆盖一切,这是一个奇怪的特殊情况. (3认同)

Dav*_*eau 7

我认为用这些抽象的例子给出一个有意义的具体答案是非常困难的......

为简化起见,有两种类型的继承:接口和实现.如果你需要继承实现,那么python与静态类型的OO语言(如C++)没那么不同.

接口的继承是存在很大差异的地方,根据我的经验对软件设计产生根本性影响.像Python这样的语言并不强制你在这种情况下使用继承,在大多数情况下避免继承是一个好点,因为以后很难修复错误的设计选择.这是任何好的OOP书中提出的一个众所周知的观点.

有些情况下,在Python中建议使用接口继承,例如插件等...对于这些情况,Python 2.5及更低版本缺乏"内置"优雅方法,并且几个大框架设计了自己的解决方案(zope,trac,twister).Python 2.6及以上版本有ABC类来解决这个问题.


小智 5

在C++/Java/etc中,多态性是由继承引起的.放弃那种不正常的信念,以及动态语言向你敞开心扉.

从本质上讲,在Python中没有任何接口,只有"理解某些方法是可调用的".漂亮的手工波浪和学术声音,不是吗?这意味着因为你称之为"说话",你清楚地期望对象应该有一个"说话"的方法.简单,对吧?这是Liskov-ian,因为类的用户定义了它的界面,这是一个很好的设计概念,可以让你进入更健康的TDD.

所以剩下的就是,另一张海报礼貌地设法避免说,代码共享技巧.您可以将相同的行为写入每个"子"类,但这将是多余的.更容易继承或混合继承层次结构中不变的功能.较小的DRY-er代码通常更好.


Lar*_*tig 5

鸭子打字毫无意义,这是接口 - 就像你在创建一个全抽象动物类时选择的接口一样.

如果你曾经使用过动物类来为其后代引入一些真实的行为,那么引入一些额外行为的狗和猫类就会有这两个类的原因.只有在祖先类没有为后代类提供实际代码的情况下,您的参数才是正确的.

因为Python可以直接知道任何对象的功能,并且因为这些功能在类定义之外是可变的,所以使用纯抽象接口"告诉"程序可以调用哪些方法的想法有点没有意义.但这不是唯一的,甚至是主要的继承点.