直接继承`object`时,是否应该调用super().__init__()?

When inheriting directly from `object`, should I call super().__init__()?

既然这道题是关于继承和super,那我们先写一道class。这是代表一个人的简单日常class:

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

        self.name = name

就像每个商品 class 一样,它在初始化自身之前调用其父构造函数。这个 class 完美地完成了它的工作;它可以毫无问题地使用:

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

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

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 class 在顶部),无法正确初始化 Centaur 实例。 Person 中的 super().__init__() 最终调用 Horse.__init__,因为缺少 fur_color 参数而抛出异常。

但是如果PersonHorse没有调用super().__init__(),这个问题就不会存在了。

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


免责声明:我知道 what super does, how the MRO works, and how super interacts with multiple inheritance。我明白是什么导致了这个错误。我只是不知道避免错误的正确方法是什么。


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

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

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

这显示了调用构造函数的顺序。另请注意,subclass 构造函数都具有兼容的签名,因为它们是在考虑彼此的情况下编写的。

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

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

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 现在是仅关键字参数。当多重继承图中的不同 classes 采用不同的参数时,几乎不可能让位置参数安全地工作,因此为了安全起见,我们需要关键字参数。 (如果多个 classes 需要使用相同的构造函数参数,情况会有所不同。)

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

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

Should classes that inherit directly from object call super().__init__()?

您似乎在搜索这个问题的简单 "yes or no" 答案,但不幸的是答案是 "it depends"。此外,在决定是否应该调用 super().__init__() 时,class 是否直接继承自 object 在某种程度上是无关紧要的。不变的是,如果 object.__init__ 被调用,它应该在没有参数的情况下被调用 - 因为 object.__init__ 不接受参数。

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

我之前提到过,一个class是否直接继承自object是一个转移话题1。但我还没有提到是什么激发了这个设计决策:如果你想让 MRO 继续搜索其他初始化程序,你应该调用 super init [read: super anymethod] [read: other anymethod ]s]。如果您想指示 MRO 搜索应在此处停止,则不应调用 super。

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

Class MRO是在编译时确定的,一般是在定义一个class的时候/导入模块的时候。但是,请注意 super 的使用涉及 runtime 分支的许多机会。你必须考虑:

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

在极少数情况下,您可能有条件地调用 super 调用。您可能会检查您的 super() 实例是否具有这个或那个属性,并围绕结果建立一些逻辑。或者,您可以调用 super(OtherClass, self) 显式 "step over" a link 并手动遍历此部分的 MRO。是的,如果默认行为不是您想要的,您可以劫持 MRO!所有这些恶魔般的想法的共同点是对 C3 linearization algorithm, how Python makes an MRO, and how super itself uses the MRO. Python's implementation was more or less lifted from another programming language, where super was named next-method 的理解。老实说,super 在 Python 中是一个非常糟糕的名字,因为它在初学者中引起了一个普遍的误解,即你总是向父 class 之一调用 "up",我希望他们选择了一个更好的名字。

定义继承层次结构时,解释器无法知道您是想重用一些其他class现有功能还是替换 它与替代实现或其他东西。任何一个决定都可能是有效且实用的设计。如果有关于何时以及如何调用 super 的硬性规定,就不会留给程序员选择——语言会把决定权从你手中夺走,自动做正确的事情。我希望这足以解释在 __init__ 中调用 super 不是一个简单的 yes/no 问题。

If yes, how would you correctly initialize SuperFoo?

(问题 this revisionFooSuperFoo 等的来源)

为了回答这一部分,我假设 MCVE 中显示的 __init__ 方法实际上需要进行一些初始化(也许您可以在问题的 MCVE 代码中添加占位符注释以实现该效果)。如果你所做的只是用相同的参数调用 super,那么根本不要定义 __init__,那没有意义。不要定义只是 pass__init__,除非你有意要在那里停止 MRO 遍历(在这种情况下,注释当然是必要的!)。

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

至于SuperFoo,它继承了SuperClsFooSuperCls 看起来是为了继承,Foo 不是。这意味着您可能有一些工作要做,正如 super harmful. One way forward, 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__(**{})

Since 99% of classes I've seen don't use **kwargs in their constructor, does that mean 99% of python classes are implemented incorrectly?

不,因为 YAGNI. Do 99% of classes need to immediately support 100% general dependency-injection with all the bells and whistles, before they are useful? Are they broken if they don't? As an example, consider the OrderedCounter recipe given in the collections docs. Counter.__init__ accepts *args and **kwargs, but doesn't proxy them in the super init call. If you wanted to use one of those arguments, well tough luck, you've got to override __init__ and intercept them. OrderedDict isn't defined cooperatively at all, really, some parent calls are hardcodeddict - 并且不会调用任何下一行的 __init__,所以任何 MRO 遍历都会在那里停止。如果您不小心将其定义为 OrderedCounter(OrderedDict, Counter) 而不是 OrderedCounter(Counter, OrderedDict),metaclass 基仍然能够创建一致的 MRO,但 class 根本不起作用作为有序计数器。

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

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

2这一点以及__new____init__之间的微妙关系在C[中得到了更详细的解释Python 来源 here.