Python继承的意义是什么?


83

假设您有以下情况

#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中,您可以将“讲话”信号发送到所需的任何对象。如果对象能够处理它,则将执行该对象,否则将引发异常。假设您向两个代码都添加了一个飞机类,并提交了一个飞机对象进行makeSpeak。在C ++情况下,它将不会编译,因为Airplane不是Animal的派生类。在Python情况下,它将在运行时引发异常,甚至可能是预期的行为。

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

我想指出的是,我还没有找到在Python中使用继承的单一原因(框架和异常树的一部分,但我想存在替代策略)。您无需实现基础派生的层次结构即可执行多态操作。如果要使用继承来重用实现,则可以通过包含和委派来完成此操作,其附加好处是可以在运行时更改它,并且可以清楚地定义所包含的接口,而不会冒意外副作用的风险。

因此,最后的问题是:Python继承的目的是什么?

编辑:感谢非常有趣的答案。确实可以将其用于代码重用,但是在重用实现时我总是要小心。通常,我倾向于做很浅的继承树,或者根本不做任何树,如果功能是通用的,我会将其重构为通用的模块例程,然后从每个对象中调用它。我确实看到了具有单个更改点的优点(例如,不添加到Dog,Cat,Moose等,而是添加到Animal,这是继承的基本优点),但是您可以通过以下方法实现相同的目的委托链(例如la JavaScript)。我并不是说这更好,只是另一种方式。

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


18
-1:“您可以通过委托链实现相同的目标”。是的,但比继承要痛苦得多。您可以完全不用任何类定义,而只需使用许多复杂的纯函数就可以实现相同的目的。您可以通过多种方法来实现同一件事,而这些方法都比继承要简单得多。
S.Lott

10
的确,我说过“我不是说会更好;)”
Stefano Borini,2009年

4
“我还没有找到在python中使用继承的唯一原因”……肯定听起来像“我的解决方案更好”。
S.Lott

9
抱歉,它给您这种印象。我的帖子旨在获得有关在python中使用继承的真实案例的正面反馈,直到今天,我仍然无法找到(主要是因为在我所有的python编程中,我都遇到了不需要这样做的情况,以及何时我确实做到了,这就是我上面解释过的情况)。
Stefano Borini

2
现实世界中的分类法很少是面向对象示例的良好基础。
阿帕拉拉2012年

Answers:


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"

假设这"speak_twice"是一个重要功能,您既不想在Dog和Cat中都对它进行编码,而且我敢肯定您可以推断出该示例。当然,您可以实现一个Python独立函数,该函数将接受一些鸭子类型的对象,检查它是否具有语音函数并调用两次,但这既不优雅,也错过了第1点(验证它是动物)。更糟糕的是,为了增强Encapsulation示例,如果后代类中的成员函数想要使用该"speak_twice"怎么办?

如果祖先类具有数据成员,例如"number_of_legs"由祖先的非抽象方法使用的数据成员(例如)"print_number_of_legs",但在后代类的构造函数中初始化(例如,Dog将其初始化为4而Snake则将其初始化),则将变得更加清晰。与0)。

同样,我敢肯定会有更多的例子,但是基本上每个基于固体面向对象设计的软件(足够大)都需要继承。


3
对于第一种情况,这意味着您正在检查类型而不是行为,这是非Python的。对于第二种情况,我同意,您基本上是在采用“框架”方法。您不仅要回收speak_twice的实现,而且不仅要回收接口,而且要进行覆盖,在考虑使用python时,您可以没有继承。
Stefano Borini,2009年

9
您可以在没有很多东西的情况下生活,例如类和函数,但是问题是什么使代码很棒。我认为继承确实可以。
罗伊·阿德勒2009年

@Stefano Borini-听起来您正在采用非常“基于规则”的方法。但是,旧的陈词滥调是真实的:它们被弄碎了。:-)
杰森·贝克

@Jason Baker-我倾向于喜欢规则,因为它们报告从经验中获得的智慧(例如错误),但是我不喜欢创造力受到规则的阻碍。所以我同意你的说法。
Stefano Borini

1
我发现这个例子不是很清楚-动物,汽车和形状的例子确实吸引了这些麻烦:) IMHO唯一重要的事情是您是否要继承实现。如果是这样,python中的规则确实类似于java / C ++;区别主要是接口的继承。在这种情况下,鸭式输入通常是解决方案-远远超过继承。
David Cournapeau 09年

12

Python中的继承全部与代码重用有关。将通用功能分解为基类,并在派生类中实现不同的功能。


11

使用Python进行继承比其他任何事情都更加方便。我发现最好将它提供给具有“默认行为”的类。

确实,有大量的Python开发人员社区反对使用继承。无论您做什么,都不要过分。具有过于复杂的类层次结构是被标记为“ Java程序员”的肯定方法,而您根本无法做到这一点。:-)


8

我认为Python中的继承不是要编译代码,这是因为继承的真正原因是将类扩展到另一个子类,并覆盖了基类中的逻辑。但是,使用Python中的鸭子输入会使“接口”概念无效,因为您可以在调用之前检查该方法是否存在,而无需使用接口来限制类结构。


3
选择性覆盖是继承的原因。如果要覆盖所有内容,这是一种奇怪的特殊情况。
美国洛特

1
谁会凌驾一切?您可以想到python,就像所有方法都是公共的和虚拟的一样
-bashmohandes

1
@bashmohandes:我永远不会覆盖一切。但是问题是一个退化的情况,一切都被覆盖了。这个奇怪的特殊情况是问题的基础。由于它在普通的OO设计中从未发生过,因此这个问题是毫无意义的。
S.Lott

7

我认为用这样的抽象例子给出有意义,具体的答案是非常困难的。

为简化起见,有两种继承:接口和实现。如果您需要继承实现,则python与静态类型的OO语言(如C ++)没有太大区别。

根据我的经验,继承接口是一个很大的不同,对您的软件设计有根本​​的影响。在这种情况下,诸如Python之类的语言不会强迫您使用继承,并且在大多数情况下避免继承是一个好主意,因为稍后很难在那里修复错误的设计选择。在任何好的OOP书中都提到了这一点。

在某些情况下,建议在Python中为接口使用继承,例如对于插件等。对于这些情况,Python 2.5及以下版本缺少“内置”优雅的方法,并且一些大型框架设计了自己的解决方案(Zope,trac,Twister)。Python 2.6及更高版本具有ABC类来解决此问题


6

鸭式打字并不是毫无意义的继承,而是接口–就像您在创建所有抽象动物类中选择的那样。

如果您使用的动物类为其子代引入了一些实际的行为,那么使用狗和猫类引入了一些其他行为,则这两个类都是有原因的。只有在祖先类没有为后代类提供实际代码的情况下,您的参数才是正确的。

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


5

在C ++ / Java / etc中,多态是由继承引起的。摒弃那些误会的信念,动态的语言向您敞开。

本质上,在Python中,没有什么接口比“了解某些方法是可调用的”更多。漂亮的波浪形和学术风格,不是吗?这意味着,因为您调用“讲话”,所以您明确希望对象应该具有“讲话”方法。简单吧?这是非常利斯科夫式的,因为类的用户定义了它的界面,这是一个好的设计概念,可带您进入更健康的TDD。

因此,正如另一位发帖人尽量避免说的那样,剩下的就是代码共享技巧。您可以在每个“子”类中写入相同的行为,但这将是多余的。继承层次结构中不变的继承或混合功能更容易。通常,较小的DRY-er代码更好。


2

我看不到继承的意义。

每当我在实际系统中使用继承时,我都会被烧死,因为它导致缠结的依赖性网络,或者我只是及时地意识到,如果没有继承,我会变得更好。现在,我尽可能避免使用它。我根本没有用。

class Repeat:
    "Send a message more than once"
    def __init__(repeat, times, do):
        repeat.times = times
        repeat.do = do

    def __call__(repeat):
        for i in xrange(repeat.times):
             repeat.do()

class Speak:
    def __init__(speak, animal):
        """
        Check that the animal can speak.

        If not we can do something about it (e.g. ignore it).
        """
        speak.__call__ = animal.speak

    def twice(speak):
        Repeat(2, speak)()

class Dog:
     def speak(dog):
         print "Woof"

class Cat:
     def speak(cat):
         print "Meow"

>>> felix = Cat()
>>> Speak(felix)()
Meow

>>> fido = Dog()
>>> speak = Speak(fido)
>>> speak()
Woof

>>> speak.twice()
Woof

>>> speak_twice = Repeat(2, Speak(felix))
>>> speak_twice()
Meow
Meow

詹姆斯·高斯林曾在一次新闻发布会上被问到一个类似的问题:“如果您可以回头再做Java,那您会遗漏什么呢?”。他的回答是“班级”,对此大笑。但是,他很认真,并解释说,确实不是问题,而是继承。

我有点把它看作是一种药物依赖-它给您快速的感觉不错,但最终,它使您感到困惑。我的意思是说,这是重用代码的便捷方法,但它会导致子类和父类之间出现不健康的耦合。对父母的更改可能会伤害孩子。子级依赖于父级来实现某些功能,并且不能更改该功能。因此,子级提供的功能也与父级绑定-您只能同时拥有两者。

更好的方法是使用在构造时组成的其他对象的功能,为实现该接口的接口提供一个面向客户端的类。通过适当设计的接口执行此操作,可以消除所有耦合,并且我们提供了高度可组合的API(这并不是什么新鲜事物-大多数程序员已经做到了,只是不够)。请注意,实现类不能简单地公开功能,否则客户端应仅直接使用组成的类-它必须通过组合该功能来做一些新的事情。

继承阵营的一个论点是,纯粹的委托实现会遭受痛苦,因为它们需要大量的“胶水”方法,这些方法只是通过委托“链”传递值。但是,这只是使用委派来重塑类似继承的设计。多年接触基于继承的设计的程序员特别容易陷入这个陷阱,因为在没有意识到的情况下,他们会思考如何使用继承来实现某些东西,然后将其转换为委托。

像上面的代码一样,关注点的正确分离不需要胶水方法,因为每个步骤实际上都是在增加价值,因此它们根本不是真正的“胶水”方法(如果它们没有增加价值,则设计有缺陷)。

归结为:

  • 对于可重用的代码,每个类只能做一件事(做得很好)。

  • 继承创建的类具有多种功能,因为它们与父类混合在一起。

  • 因此,使用继承会使类难以重用。


1

您可以绕过Python和几乎所有其他语言的继承。但这全都与代码重用和代码简化有关。

这只是一个语义技巧,但是在构建了类和基类之后,您甚至不必知道对象可以做什么,看看是否可以做到。

假设您有d,它是将Animal归为子类的Dog。

command = raw_input("What do you want the dog to do?")
if command in dir(d): getattr(d,command)()

如果用户键入的内容可用,则代码将运行适当的方法。

使用此功能,您可以创建想要的哺乳动物/爬行动物/鸟类混合怪物的任意组合,现在您可以将其设置为“ Bark!”。在飞行并伸出叉状舌头时,它会正确处理!玩得开心!


1

另一个小问题是op的第3个示例,您不能调用isinstance()。例如,将您的第3个示例传递给另一个对象,然后该对象通过“动物”(Animal)键入一个在其上说话的电话。如果不这样做,则必须检查狗的类型,猫的类型等。由于绑定晚,因此不确定实例检查是否真的是“ Pythonic”。但是,然后您必须实现某种方式,即动物芝士不说话,因此AnimalControl不会尝试将芝士汉堡类型扔进卡车。

class AnimalControl(object):
    def __init__(self):
        self._animalsInTruck=[]

    def catachAnimal(self,animal):
        if isinstance(animal,Animal):
            animal.speak()  #It's upset so it speak's/maybe it should be makesNoise
            if not self._animalsInTruck.count <=10:
                self._animalsInTruck.append(animal) #It's then put in the truck.
            else:
                #make note of location, catch you later...
        else:
            return animal #It's not an Animal() type / maybe return False/0/"message"

0

Python中的类基本上只是将一堆函数和数据进行分组的方式。它们不同于C ++等中的类。

我主要看到继承用于超类的重写方法。例如,也许更多的Python使用继承就可以了。

from world.animals import Dog

class Cat(Dog):
    def speak(self):
        print "meow"

当然,猫不是狗的类型,但我有一个(第三方)Dog类,它可以很好地工作,除了speak我要覆盖的方法-这样可以省去重新实现整个类的时间,因此很高兴。同样,虽然Cat不是的类型Dog,但是猫的确继承了很多属性。

重写方法或属性的一个更好的(实际)示例是如何更改urllib的用户代理。您基本上是子类urllib.FancyURLopener并更改version属性(来自文档):

import urllib

class AppURLopener(urllib.FancyURLopener):
    version = "App/1.7"

urllib._urlopener = AppURLopener()

当以更“正确”的方式使用继承时,使用异常的另一种方式是“异常”:

class AnimalError(Exception):
    pass

class AnimalBrokenLegError(AnimalError):
    pass

class AnimalSickError(AnimalError):
    pass

..然后,您AnimalError可以捕获所有从其继承的异常,或特定的异常 AnimalBrokenLegError


6
我……对您的第一个例子有些困惑。最后我检查了一下,猫不是狗,所以我不确定你想证明什么样的关系。:-)
Ben Blank

1
您搞砸了Liskov原则:猫不是狗。在这种情况下可以使用,但是如果Dog类更改并获得例如对猫来说毫无意义的“ Lead”字段,该怎么办?
德米特里·里森贝格

1
好吧,如果没有Animal基类,那么您的替代方法是重新编写整个过程。.我并不是说这是最佳实践(如果有Animal基类,请使用它),但是它可以正常工作并且经常使用(按照我添加的示例,这是更改urllib的用户代理的推荐方法)
dbr
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.