Python中的继承点是什么?

时间:2009-06-19 23:28:19

标签: 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接口,因为它只包含一个纯虚方法。 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)

现在,在此示例中,您会看到相同的策略。您使用继承来利用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)

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

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

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

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

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

我在这方面也找到了a similar post

11 个答案:

答案 0 :(得分:78)

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

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

首先,如果你明智地编码,你的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独立函数,它将接受一些duck-typed对象,检查它是否有一个speak函数并调用它两次,但这都是非优雅的并且错过了第一个点(验证它是一个Animal)。更糟糕的是,为了加强Encapsulation示例,如果后代类中的成员函数想要使用"speak_twice"该怎么办?

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

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

答案 1 :(得分:12)

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

答案 2 :(得分:10)

Python中的继承比其他任何东西都更方便。我发现它最适合用于提供具有“默认行为”的类。

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

答案 3 :(得分:8)

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

答案 4 :(得分:7)

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

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

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

有些情况下,在Python中建议使用接口继承,例如插件等......对于这些情况,Python 2.5及更低版本缺乏“内置”优雅方法,并设计了几个大框架他们自己的解决方案(zope,trac,twister)。 Python 2.6及更高版本有ABC classes to solve this

答案 5 :(得分:5)

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

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

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

答案 6 :(得分:5)

鸭子打字毫无意义,它的界面就像你在创建一个全抽象动物类时所选择的那样,这不是继承。

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

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

答案 7 :(得分:1)

你可以用Python和几乎任何其他语言来解决继承问题。这完全是关于代码重用和代码简化的。

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

假设你有一个将动物分类的狗。

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

如果用户输入的内容可用,代码将运行正确的方法。

使用它你可以创建你想要的任何Mammal / Reptile / Bird混合怪物的组合,现在你可以说它'Bark!'飞行并伸出它的叉形舌头,它将正确处理它!玩得开心!

答案 8 :(得分:1)

我没有看到继承的重点。

每次我在真实系统中使用继承时,我都会被烧毁,因为它导致了一个纠结的依赖网络,或者我只是及时意识到没有它我会好多了。现在,我尽可能地避免它。我根本就没用过它。

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

詹姆斯·戈斯林曾在新闻发布会上被问到一个问题:“如果你能以不同的方式回归,那么你会忘掉什么?”他的回答是“课堂”,有笑声。然而,他是认真的,并解释说,实际上,这不是课程,而是继承。

我认为它像药物依赖 - 它给你一个感觉很好的快速修复,但最终,它会让你感到困惑。我的意思是,这是一种重用代码的便捷方式,但它会强制子类和父类之间的不健康耦合。对父母的更改可能会破坏孩子。对于某些功能,子进程依赖于父进程,并且无法更改该功能。因此,孩子提供的功能也与父母有关 - 你只能同时拥有两者。

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

继承阵营的论点是纯粹的委托实现受到影响,因为它们需要大量的“粘合”方法,这些方法只是通过委托“链”传递值。但是,这只是使用委托重新发明类似继承的设计。多年接触基于继承的设计的程序员特别容易陷入这个陷阱,因为没有意识到,他们会想到如何使用继承实现某些东西,然后将其转换为委托。

正确分离上述代码之类的问题不需要粘合方法,因为每个步骤实际上都是添加值,因此它们根本不是真正的“粘合”方法(如果它们不是增值,设计有缺陷。)

归结为:

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

  • 继承会创建可以执行的类 不止一件事,因为他们是 与父类混淆。

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

答案 9 :(得分:1)

另一个小问题是op的第3个例子,你不能调用isinstance()。例如,将您的第3个示例传递给另一个带有“动物”类型的对象,并在其上进行通话。如果你这样做,你不必检查狗类型,猫类型等。由于后期绑定,不确定实例检查是否真的是“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"

答案 10 :(得分:0)

Python中的类基本上只是对一堆函数和数据进行分组的方法。它们与C ++中的类不同......

我主要看到用于覆盖超类方法的继承。例如,也许更多Python使用继承将是..

from world.animals import Dog

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

当然猫不是一种狗,但我有这个(第三方)Dog类完美地运作,除了我想要的speak方法覆盖 - 这节省了重新实现整个类,只是它喵喵叫。同样,虽然Cat不是Dog的类型,但是猫确实继承了很多属性。

覆盖方法或属性的更好(实用)示例是如何更改urllib的用户代理。您基本上是urllib.FancyURLopener的子类并更改版本属性(from the documentation):

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

这样的特定异常