【问题标题】:What’s the point of inheritance in Python?Python中的继承有什么意义?
【发布时间】:2010-11-04 11:05:18
【问题描述】:

假设你有以下情况

#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 都是 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 中并为每个对象实现接口。有很多解决方案...

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

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

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

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

【问题讨论】:

  • -1:“您可以通过委托链实现相同的目的”。没错,但比继承要痛苦得多。您完全可以在不使用任何类定义的情况下实现相同的功能,只需使用大量复杂的纯函数即可。你可以用十几种方法来实现同样的事情,都比继承简单。
  • 确实我说过“我不是说它更好;)”
  • “我还没有找到在 python 中使用继承的单一理由”...听起来确实像“我的解决方案更好”。
  • 对不起,如果它给你这个印象。我的帖子旨在获得关于在 python 中使用继承的真实案例故事的积极反馈,截至今天,我无法找到(主要是因为在我的所有 python 编程中,我遇到了不需要这样做的情况,以及何时我做了,就是我上面解释的情况)。
  • 现实世界的分类法很少成为面向对象示例的良好基础。

标签: python oop inheritance


【解决方案1】:

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

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

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

其次,显然更直接的是封装——面向对象设计的另一个组成部分。当祖先具有数据成员和/或非抽象方法时,这变得相关。举一个愚蠢的例子,其中祖先有一个调用 then-abstract 函数的函数 (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 独立函数,该函数将接受一些鸭子类型的对象,检查它是否有一个 speak 函数并调用它两次,但这既不优雅,也错过了第 1 点(验证它是一个动物)。更糟糕的是,为了加强封装示例,如果后代类中的成员函数要使用"speak_twice",该怎么办?

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

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

【讨论】:

  • 对于第一种情况,这意味着您正在检查类型而不是行为,这有点不合pythonic。对于第二种情况,我同意,你基本上是在做“框架”的方法。你在回收 speak_twice 的实现,不仅是接口,而且为了覆盖,你在考虑 python 时可以不用继承。
  • 你可以在没有很多东西的情况下生活,比如类和函数,但问题是是什么让代码变得很棒。我认为继承可以。
  • @Stefano Borini - 听起来您正在采用一种非常“基于规则”的方法。不过,陈词滥调是真的:它们是被打破的。 :-)
  • 我觉得这个例子不太清楚——动物、汽车和形状的例子真的很讨厌这些讨论:) 恕我直言,唯一重要的是你是否想继承实现。如果是这样,python 中的规则确实类似于 java/C++;区别主要在于接口的继承。在这种情况下,duck-typing 通常是解决方案——比继承更重要。
  • 我有点不同意你应该使用继承来获得更多的属性或功能。正如原作者所提到的,浅继承和实用程序类/函数而不是将它们放在基类中,在长期维护方面要好得多。当然,为了快速而肮脏,获取现有类、子类并添加更多功能总是很容易,但它会变得混乱而且是错误的。我似乎已经有足够多的系统,其中层次结构很深,无法遵循。甚至在派生中引入了 dup 成员的 Android 代码中也看到过。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

    确实,有一个重要的 Python 开发者社区反对使用继承。无论你做什么,不要只是不要过度。拥有一个过于复杂的类层次结构是被贴上“Java 程序员”标签的可靠方法,而你就是不能这样。 :-)

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 选择性覆盖是继承的原因。如果您要覆盖所有内容,那是一个奇怪的特殊情况。
      • 谁会覆盖一切?你可以把 python 想象成所有的方法都是公共的和虚拟的
      • @bashmohandes:我永远不会覆盖所有内容。但是这个问题显示了一种退化的情况,即一切都被覆盖了。这个奇怪的特殊情况是问题的基础。由于它在正常的 OO 设计中从未发生过,因此这个问题毫无意义。
      【解决方案5】:

      我认为用这样抽象的例子很难给出一个有意义、具体的答案......

      为了简化,继承有两种类型:接口和实现。如果您需要继承实现,那么 python 与 C++ 等静态类型的 OO 语言并没有太大区别。

      接口的继承是一个很大的区别,根据我的经验,它会对您的软件设计产生根本性的影响。在这种情况下,像 Python 这样的语言不会强迫您使用继承,并且在大多数情况下避免继承是一个好点,因为以后很难修复错误的设计选择。这是任何优秀的 OOP 书籍中都提出的一个众所周知的观点。

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

      【讨论】:

        【解决方案6】:

        duck-typed 变得毫无意义的不是继承,而是接口——就像您在创建全抽象动物类时选择的接口。

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

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

        【讨论】:

          【解决方案7】:

          在 C++/Java/etc 中,多态是由继承引起的。放弃这种错误的信念,动态语言向你敞开大门。

          本质上,在 Python 中没有接口,而是“理解某些方法是可调用的”。漂亮的手波和学术听起来,不是吗?这意味着因为您调用“speak”,您显然希望该对象应该具有“speak”方法。很简单吧?这是非常 Liskov 式的,因为类的用户定义了它的接口,这是一个很好的设计理念,可以引导您进入更健康的 TDD。

          剩下的就是,正如另一位发帖人礼貌地避免说的那样,代码共享技巧。您可以将相同的行为写入每个“子”类,但这将是多余的。更容易继承或混合在继承层次结构中不变的功能。一般来说,更小、更干燥的代码更好。

          【讨论】:

            【解决方案8】:

            我认为继承没有多大意义。

            每次我在实际系统中使用继承时,我都会被烧死,因为它会导致一个错综复杂的依赖网络,或者我只是及时意识到如果没有它我会好很多。现在,我尽可能地避免它。我根本没用过。

            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
            

            James Gosling 曾在一次新闻发布会上被问到一个类似的问题:“如果你可以回去用不同的方式做 Java,你会遗漏什么?”。他的回答是“上课”,引起了哄堂大笑。但是,他很认真地解释说,实际上,问题不在于类,而在于继承。

            我觉得它有点像药物依赖——它给你一个感觉很好的快速修复,但到最后,它会让你一团糟。我的意思是这是一种重用代码的便捷方式,但它会在子类和父类之间产生不健康的耦合。对父母的改变可能会破坏孩子。孩子依赖于父母的某些功能并且不能改变该功能。因此,子级提供的功能也与父级绑定 - 您只能同时拥有两者。

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

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

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

            归结为:

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

            • 继承创建的类 不止一件事,因为它们是 与父类混在一起。

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

            【讨论】:

              【解决方案9】:

              您可以在 Python 和几乎任何其他语言中绕过继承。不过,这都是关于代码重用和代码简化的。

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

              假设你有 d,它是一个 Dog,它是 Animal 的子类。

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

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

              使用它,您可以创建任何您想要的哺乳动物/爬行动物/鸟类混合怪物组合,现在您可以让它说“吠叫!”一边飞,一边伸出分叉的舌头,它会妥善处理它!玩得开心!

              【讨论】:

                【解决方案10】:

                另一个小点是 op 的第三个例子,你不能调用 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"
                

                【讨论】:

                  【解决方案11】:

                  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 这样的特定异常

                  【讨论】:

                  • 我……对你的第一个例子有点困惑。最后我查了一下,猫不是一种狗,所以我不确定你想证明什么关系。 :-)
                  • 你在搞乱里氏原理:猫不是狗。在这种情况下使用可能没问题,但是如果 Dog 类发生变化并获得例如对猫来说毫无意义的“Lead”字段怎么办?
                  • 好吧,如果没有 Animal 基类,您的选择是重新实现整个事情。我并不是说这是最佳实践(如果有 Animal 基类,请使用它),但它可以工作并且被普遍使用(根据我添加的示例,这是更改 urllib 的用户代理的推荐方法)
                  猜你喜欢
                  • 2021-11-13
                  • 2014-05-05
                  • 1970-01-01
                  • 1970-01-01
                  • 2012-04-15
                  • 1970-01-01
                  • 2012-06-03
                  • 2013-08-05
                  • 2018-11-21
                  相关资源
                  最近更新 更多