【问题标题】:When inheriting directly from `object`, should I call super().__init__()?当直接从 `object` 继承时,我应该调用 super().__init__() 吗?
【发布时间】:2018-09-11 09:27:02
【问题描述】:

由于这个问题是关于继承和super,让我们从编写一个类开始。这是一个代表一个人的简单日常类:

class Person:
    def __init__(self, name):
        super().__init__()

        self.name = name

就像每个好的类应该在初始化之前调用它的父构造函数。这门课做得很好;它可以毫无问题地使用:

>>> Person('Tom')
<__main__.Person object at 0x7f34eb54bf60>

但是当我尝试创建一个继承自 Person 和另一个类的类时,事情突然出错了:

class Horse:
    def __init__(self, fur_color):
        super().__init__()

        self.fur_color = fur_color

class Centaur(Person, Horse):
    def __init__(self, name, fur_color):
        # ??? now what?
        super().__init__(name)  # throws TypeError: __init__() missing 1 required positional argument: 'fur_color'
        Person.__init__(self, name)  # throws the same error

由于菱形继承(object 类位于顶部),无法正确初始化 Centaur 实例。 Person 中的 super().__init__() 最终调用 Horse.__init__,由于缺少 fur_color 参数而引发异常。

但如果PersonHorse 不调用super().__init__(),则不会存在此问题。

这提出了一个问题:直接从object 继承的类是否应该调用super().__init__()?如果是,您将如何正确初始化Centaur


免责声明:我知道what super doesMRO 的工作原理以及how super interacts with multiple inheritance。我了解导致此错误的原因。我只是不知道避免错误的正确方法是什么。


为什么我要专门询问object,即使菱形继承也可以发生在其他类中?那是因为object 在 python 的类型层次结构中有一个特殊的位置——不管你喜欢与否,它都位于 MRO 的顶部。通常菱形继承仅在您故意从某个基类继承以实现与该类相关的某个目标时发生。在这种情况下,钻石继承是可以预料的。但是如果菱形顶部的类是object,那么很可能你的两个父类完全不相关并且有两个完全不同的接口,所以出错的可能性更高。

【问题讨论】:

  • 我不完全理解你的问题。但是,如果您正在设计一个要在多重继承场景中使用的类 - 并且可能在任何情况下 - 您应该始终接受 *args, **kwargs 并在任何超级调用中传递它们。
  • @DanielRoseman 问题基本上是这样的:我已经确定了一个由使用super引起的问题(大概),所以我想知道调用super是否不好。答案应该是这样的:“使用super 不好,因为你在问题中提出的确切原因”“使用super 没有任何问题,前提是你的课程是经过设计的用于多重协作继承”等。或者换一种说法,这个问题可以认为是“直接从object继承的类在什么情况下应该使用super?”
  • 有人可能会问为什么object.__init__() 不接受*args**kwargs,这将解决问题。
  • @brunodesthuilliers 会吗?我不认为 object.__init__ 在这段代码中的任何地方都用参数调用,是吗?

标签: python inheritance super


【解决方案1】:

直接从object 继承的类是否应该调用super().__init__()

您似乎在搜索这个问题的简单“是或否”答案,但不幸的是答案是“视情况而定”。此外,在决定是否应该调用super().__init__() 时,一个类是否直接继承自object 在某种程度上是无关紧要的。不变的是,如果调用object.__init__,则应该不带参数调用它——因为object.__init__ 不接受参数。

实际上,在协作继承情况下,这意味着您必须确保在调用object.__init__ 之前使用所有参数。它确实意味着你应该尽量避免object.__init__被调用。 Here 是在调用 super 之前使用 args 的示例,responserequest 上下文已从可变映射 kwargs 中弹出。

我之前提到过,一个类是否直接从object 继承是一个红鲱鱼1。但是我还没有提到应该激发这个设计决策的原因:如果您希望 MRO 继续搜索其他初始化程序 [阅读:其他 anymethods],则应该调用 super init [阅读:super anymethod]。如果您想指示应在此处停止 MRO 搜索,则不应调用 super。

如果object.__init__ 什么都不做,为什么它还存在呢?因为它确实做了一些事情:确保它在没有参数的情况下被调用。参数的存在可能表明存在错误2object 也用于停止超级调用链 - somebody 必须不调用 super,否则我们将无限递归。您可以更早地通过不调用 super 来明确地停止它。如果你不这样做,object 将作为最后一环,为你停止链条。

类 MRO 是在编译时确定的,通常是在定义类时/导入模块时。但是,请注意super 的使用涉及许多runtime 分支的机会。您必须考虑:

  • super 的方法使用哪些参数调用(即,您希望沿 MRO 转发哪些参数)
  • 无论是否调用 super(有时您想故意破坏链条)
  • super本身是用哪些参数(如果有的话)创建的(下面描述了一个高级用例)
  • 是否在您自己方法中的代码之前之后调用代理方法(如果需要访问代理方法设置的某些状态,请先放置super,放置super最后,如果您要设置代理方法所依赖的某种状态 - 在某些情况下,您甚至希望将 super 放在中间某处!)
  • 问题主要是关于__init__,但不要忘记 super 也可以与任何其他方法一起使用

在极少数情况下,您可能会有条件地调用super。您可能会检查您的 super() 实例是否具有这个或那个属性,并围绕结果建立一些逻辑。或者,您可以调用super(OtherClass, self) 显式“跳过”链接并手动遍历此部分的 MRO。是的,如果默认行为不是您想要的,您可以劫持 MRO!所有这些恶魔般的想法的共同点是对C3 linearization算法的理解,Python如何制作MRO,以及super本身如何使用MRO。 Python 的实现或多或少源自another programming language,其中super 被命名为next-method。老实说,super 在 Python 中是一个非常糟糕的名称,因为它在初学者中造成了一个普遍的误解,即您总是“向上”调用其中一个父类,我希望他们选择了一个更好的名称。

在定义继承层次结构时,解释器无法知道您是想重用一些其他类现有的功能还是替换替代实现,或其他.任一决定都可能是有效且实用的设计。如果有一个关于何时以及如何调用super 的硬性规则,它不会留给程序员选择 - 语言将让你做出决定并自动做正确的事情。我希望这足以说明在 __init__ 中调用 super 并不是一个简单的是/否问题。

如果是,您将如何正确初始化SuperFoo

(问题的this revisionFooSuperFoo等的来源)

为了回答这一部分,我假设 MCVE 中显示的 __init__ 方法实际上需要进行一些初始化(也许您可以在问题的 MCVE 代码中添加占位符 cmets 来达到此目的)。根本不要定义__init__,如果你唯一要做的就是用相同的参数调用super,那就没有意义了。不要将__init__ 定义为pass,除非您有意在此处停止MRO 遍历(在这种情况下,肯定需要添加注释!)。

首先,在我们讨论SuperFoo 之前,让我说NoSuperFoo 看起来像是一个不完整或糟糕的设计。如何将foo 参数传递给Foo 的init? 3foo 初始值是硬编码的。硬编码(或以其他方式自动确定)foo 的初始值可能是可以的,但随后 you should probably be doing composition not inheritance

对于SuperFoo,它继承了SuperClsFooSuperCls 看起来是为了继承,Foo 不是。这意味着您可能有一些工作要做,正如super harmful 中所指出的那样。一种前进的方式,as discussed in Raymond's blog,是编写适配器。

class FooAdapter:
    def __init__(self, **kwargs):
        foo_arg = kwargs.pop('foo')  
        # can also use kwargs['foo'] if you want to leave the responsibility to remove 'foo' to someone else
        # can also use kwargs.pop('foo', 'foo-default') if you want to make this an optional argument
        # can also use kwargs.get('foo', 'foo-default') if you want both of the above
        self._the_foo_instance = Foo(foo_arg)
        super().__init__(**kwargs)

    # add any methods, wrappers, or attribute access you need

    @property
    def foo():
        # or however you choose to expose Foo functionality via the adapter
        return self._the_foo_instance.foo

注意FooAdapter 有一个 Foo,而不是FooAdapter 是一个 Foo。这不是唯一可能的设计选择。但是,如果您像class FooParent(Foo) 一样继承,那么您就是在暗示FooParent Foo,并且可以在Foo 本来是的任何地方使用 - 它是通过使用组合,通常更容易避免违反LSPSuperCls 也应该通过允许 **kwargs 合作:

class SuperCls:
    def __init__(self, **kwargs):
        # some other init code here
        super().__init__(**kwargs)

也许SuperCls 也是你无法控制的,你也必须适应它,就这样吧。关键是,这是一种重用代码的方法,通过调整接口使签名匹配。假设每个人都很好地合作并消费他们需要的东西,最终super().__init__(**kwargs) 将代理到object.__init__(**{})

由于我见过的 99% 的类在其构造函数中没有使用 **kwargs,这是否意味着 99% 的 python 类实现不正确?

不,因为YAGNI。 99% 的类是否需要立即支持 100% 的通用依赖注入以及所有的花里胡哨,然后才能有用?如果他们不这样做,他们会坏吗?例如,考虑集合文档中给出的OrderedCounter 配方。 Counter.__init__ 接受 *args**kwargs,但接受 doesn't proxy them in the super init call。如果您想使用其中一个参数,那么运气不好,您必须覆盖 __init__ 并拦截它们。 OrderedDict 根本没有合作定义,实际上,一些 parent calls are hardcodeddict - 并且不会调用任何下一行的 __init__,因此任何 MRO 遍历都会在那里停止。如果您不小心将其定义为 OrderedCounter(OrderedDict, Counter) 而不是 OrderedCounter(Counter, OrderedDict),元类基础仍然能够创建一致的 MRO,但该类根本无法用作有序计数器。

尽管存在所有这些缺点,@​​987654412@ 配方仍能像宣传的那样工作,因为 MRO 是按照为预期用例设计的而遍历的。因此,您甚至不需要 100% 正确地进行协作继承来实现依赖注入。这个故事的寓意是完美是进步的敌人(或者,practicality beats purity)。如果您想将MyWhateverClass 塞进任何您可以梦想的疯狂继承树中,请继续,但您需要编写必要的脚手架来实现这一点。像往常一样,Python 不会阻止你以任何足够好的方式来实现它。

1无论你是否在类声明中编写它,你总是从对象继承。为了与 2.7 运行时交叉兼容,许多开源代码库无论如何都会显式继承自对象。

2CPython 源代码here 中更详细地解释了这一点,以及__new____init__ 之间的微妙关系。

【讨论】:

  • Bleh,OrderedCounter 食谱。它完全可以工作的实现细节的侥幸,以及它不能与交换的基类一起工作的实现细节的侥幸,并且无法从 OrderedDictCounter 文档中判断应该使用哪个顺序或者不应该工作。
  • 所以总结一下:你做什么都没关系;如果您的子类对您的类的接口有问题,他们应该编写一个适配器。我理解正确吗?
  • 这是一个判断调用 - 如果子类是您自己的子类,您可能会发现自己想要重构类接口以允许更容易继承,而不是编写适配器。如果你能做得好,适配器可能根本就不需要了。但是您无法通过第三方代码预测所有可能的用例。
  • @user2357112 你可能会喜欢这个artist's depiction of the OrderedCounter recipe
【解决方案2】:

如果PersonHorse 从未设计为用作同一类的基类,那么Centaur 可能不应该存在。正确设计多重继承非常困难,不仅仅是调用super。即使是单继承也很棘手。

如果 PersonHorse 应该支持创建像 Centaur 这样的类,那么 PersonHorse(可能还有它们周围的类)需要重新设计。这是一个开始:

class Person:
    def __init__(self, *, name, **kwargs):
        super().__init__(**kwargs)
        self.name = name

class Horse:
    def __init__(self, *, fur_color, **kwargs):
        super().__init__(**kwargs)
        self.fur_color = fur_color

class Centaur(Person, Horse):
    pass

stevehorse = Centaur(name="Steve", fur_color="brown")

您会注意到一些变化。让我们来看看列表。

首先,__init__ 签名现在在中间有一个** 标记仅关键字参数的开始:namefur_color 现在是仅关键字参数。当多重继承图中的不同类采用不同的参数时,几乎不可能让位置参数安全地工作,因此为了安全起见,我们需要关键字参数。 (如果多个类需要使用相同的构造函数参数,情况会有所不同。)

其次,__init__ 签名现在全部采用**kwargs。这让Person.__init__ 接受它不理解的关键字参数,例如fur_color,并将它们传递到下一行,直到它们到达任何理解它们的类。当参数达到object.__init__ 时,object.__init__ 应该收到空的kwargs

第三,Centaur 不再有自己的__init__。重新设计了PersonHorse,它不需要__init__。从Person 继承的__init__ 将对Centaur 的MRO 做正确的事情,将fur_color 传递给Horse.__init__

【讨论】:

  • 很好的答案,无疑是正确的。但是如果可能的话,如果您可以扩展“它们永远不应该用作基类”的情况,那就太好了。如果PersonHorse 不是为支持Centaur 而设计的,我该怎么办?我是否应该编写自己的 PersonHorse 类并考虑多重协作继承?我应该为现有的PersonHorse 类编写适配器吗?或者可能是我没有想到的其他选择?
  • 在现实世界中,前进的道路取决于是否有其他人在使用PersonHorse,并且您需要保持向后兼容。
【解决方案3】:

当使用super调用超类方法时,通常需要当前类和当前实例作为参数:

super(Centaur, self).__init__(...)

现在,问题在于 Python 处理 walking the superclasses 的方式。如果__init__ 方法没有匹配的签名,那么调用可能会导致问题。从链接的例子:

class First(object):
    def __init__(self):
        print "first prologue"
        super(First, self).__init__()
        print "first epilogue"

class Second(First):
    def __init__(self):
            print "second prologue"
            super(Second, self).__init__()
            print "second epilogue"

class Third(First):
    def __init__(self):
        print "third prologue"
        super(Third, self).__init__()
        print "third epilogue"

class Fourth(Second, Third):
    def __init__(self):
        super(Fourth, self).__init__()
        print "that's it"

Fourth()

输出是:

$ python2 super.py                                                                                                                                                                      
second prologue
third prologue
first prologue
first epilogue
third epilogue
second epilogue
that's it

这显示了构造函数的调用顺序。另请注意,子类构造函数都具有兼容的签名,因为它们是相互编写的。

【讨论】:

  • super(Centaur, self) 仅等同于super(),因此确实没有理由显式传递参数。是的,我了解 MRO 的工作原理以及它是如何导致该问题的。问题是:我该怎么办?规避这些问题的正确方法是什么?您的回答根本没有解决这个问题。
  • 我认为链接答案中的代码更好地说明了这一点。
猜你喜欢
  • 2018-11-06
  • 2021-11-23
  • 2010-12-12
  • 2020-07-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-28
  • 2014-01-30
相关资源
最近更新 更多