当直接从`object`继承时,我应该调用super().__init__()吗?

2024-03-27

由于这个问题是关于继承和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实例正确。这super().__init__() in Person最终打电话Horse.__init__,这会引发异常,因为fur_color缺少参数。

但这个问题不会存在,如果Person and Horse没有打电话super().__init__().

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


免责声明:我知道what super does https://stackoverflow.com/questions/222877/what-does-super-do-in-python,如何MRO https://docs.python.org/3/glossary.html#term-method-resolution-order作品,以及how super与多重继承相互作用 https://stackoverflow.com/q/3277367/1222951。我明白是什么导致了这个错误。我只是不知道避免错误的正确方法是什么。


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


应该直接继承自的类object call super().__init__()?

您似乎在寻找这个问题的简单“是或否”答案,但不幸的是答案是“这取决于”。此外,在决定是否应该致电super().__init__(),一个类是否直接继承自有点无关紧要object。不变的是,如果object.__init__被调用时,应该不带参数地调用它 - 因为object.__init__不接受争论。

实际上,在合作继承的情况下,这意味着您必须确保所有参数都是consumed before object.__init__被调用。确实如此not意味着你应该尽量避免object.__init__被调用。Here https://github.com/requests/requests/blob/943a5c8e89db1758ae24adbbedacb3b05c32df4a/requests/exceptions.py#L19-L21是在调用之前消耗 args 的示例super, the response and request上下文已从可变映射中弹出kwargs.

I mentioned earlier that whether or not a class inherits directly from object is a red herring1. But I didn't mention yet what should motivate this design decision: You should call super init [read: super anymethod] if you want the MRO to continue to be searched for other initializers [read: other anymethods]. You should not invoke super if you want to indicate the MRO search should be stopped here.

Why does object.__init__ exist at all, if it doesn't do anything? Because it does do something: ensures it was called without arguments. The presence of arguments likely indicates a bug2. object also serves the purpose of stopping the chain of super calls - somebody has to not call super, otherwise we recurse infinitely. You can stop it explicitly yourself, earlier, by not invoking super. If you don't, object will serve as the final link and stop the chain for you.

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

  • 其中参数的方法super被调用(即您想要沿着 MRO 转发哪些参数)
  • 无论 super 是否被调用(有时你想故意打破链条)
  • 哪些参数(如果有)super itself创建于(下面描述了一个高级用例)
  • 是否调用代理方法before or after您自己的方法中的代码(如果需要访问代理方法设置的某些状态,请将 super 放在第一位;如果您要设置代理方法依赖于已经存在的某些状态,则将 super 最后放在最后 - 在某些情况下,您甚至想要将 super 放在中间某处!)
  • 这个问题主要问的是__init__,但不要忘记 super 也可以与任何其他方法一起使用

在极少数情况下,您可能有条件地调用super称呼。您可以检查一下您的super()实例具有这个或那个属性,并围绕结果建立一些逻辑。或者,您可能会调用super(OtherClass, self)显式“跳过”链接并手动遍历此部分的 MRO。是的,如果默认行为不是您想要的,您可以劫持 MRO!所有这些邪恶的想法的共同点是对C3线性化 https://en.wikipedia.org/wiki/C3_linearization算法,Python如何制作MRO,以及super本身如何使用MRO。 Python 的实现或多或少是从另一种编程语言 https://en.wikipedia.org/wiki/Dylan_(programming_language),其中 super 被命名为next-method https://opendylan.org/documentation/intro-dylan/objects.html。诚实地super在Python中是一个非常糟糕的名字,因为它在初学者中引起了一个常见的误解,即你总是调用“up”到父类之一,我希望他们选择了一个更好的名字。

当定义继承层次结构时,解释器无法知道你是否想要reuse一些其他类的现有功能或replace它与替代实现,或其他东西。任何一个决定都可能是有效且实用的设计。如果有一个关于何时以及如何的硬性规则super应该被调用,它不会留给程序员来选择 - 语言会将决定权从你手中夺走,并自动执行正确的操作。我希望这足以解释在中调用 super__init__不是一个简单的是/否的问题。

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

(Source for Foo, SuperFoo etc in this revision https://stackoverflow.com/revisions/63340da2-7e93-4c21-9cd0-f39a051e4b1a/view-source of the question)

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

首先,在我们讨论之前SuperFoo,让我这么说NoSuperFoo看起来像是不完整或糟糕的设计。你如何通过foo论证Foo的初始化?这foo的初始值3被硬编码。硬编码(或以其他方式自动确定) foo 的 init 值可能没问题,但随后你可能应该进行组合而不是继承 https://en.wikipedia.org/wiki/Composition_over_inheritance.

As for SuperFoo,它继承了SuperCls and Foo. SuperCls看起来是为了继承,Foo才不是。这意味着您可能有一些工作要做,如中所述超级有害 https://fuhm.net/super-harmful/。前进的一条路,正如雷蒙德博客中所讨论的 https://rhettinger.wordpress.com/2011/05/26/super-considered-super/,正在编写适配器。

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 has a Foo, not FooAdapter is a Foo。这不是唯一可能的设计选择。但是,如果您像这样继承class FooParent(Foo),那么你就暗示了FooParent is a Foo,并且可以在任何需要的地方使用Foo否则会更容易避免违反LSP https://en.wikipedia.org/wiki/Liskov_substitution_principle通过使用组合。SuperCls还应通过允许进行合作**kwargs:

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

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

因为我见过的 99% 的类都没有使用**kwargs在他们的构造函数中,这是否意味着 99% 的 python 类都被错误地实现了?

没有为什么YAGNI https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it。 99% 的类是否需要立即支持 100% 通用依赖注入以及所有附加功能才能发挥作用?如果不这样做,它们就坏了吗?作为一个例子,考虑OrderedCounter https://docs.python.org/3/library/collections.html#ordereddict-examples-and-recipes集合文档中给出的配方。Counter.__init__ https://github.com/python/cpython/blob/998b80636690ffbdb0a278810d9c031fad38631d/Lib/collections/__init__.py#L548接受*args and **kwargs, but 不在超级初始化调用中代理它们 https://github.com/python/cpython/blob/998b80636690ffbdb0a278810d9c031fad38631d/Lib/collections/__init__.py#L565。如果你想使用这些参数之一,那么运气不好,你必须重写__init__并拦截他们。OrderedDict https://github.com/python/cpython/blob/d13e59c1b512069d90efe7ee9b613d3913e79c56/Lib/collections/__init__.py#L96根本不是合作定义的,真的,有些父调用是硬编码的 https://github.com/python/cpython/blob/d13e59c1b512069d90efe7ee9b613d3913e79c56/Lib/collections/__init__.py#L116 to dict- 以及__init__队列中接下来的任何内容都不会被调用,因此任何 MRO 遍历都会在那里停止。如果你不小心将其定义为OrderedCounter(OrderedDict, Counter)代替OrderedCounter(Counter, OrderedDict)元类基仍然能够创建一致的 MRO,但该类根本无法作为有序计数器工作。

尽管存在所有这些缺点,OrderedCounter配方如宣传的那样工作,因为 MRO 是按照针对预期用例设计的方式进行遍历的。因此,您甚至不需要 100% 正确地执行合作继承来实现依赖项注入。这个故事的寓意是完美是进步的敌人 (or, 实用胜过纯粹 https://github.com/python/cpython/blob/acd282fd5b3ca4de302b33c9361dbc433593c4ca/Lib/this.py#L11)。如果你想补习MyWhateverClass进入任何你可以想象的疯狂的继承树,继续,但是你需要编写必要的脚手架来实现这一点。与往常一样,Python 不会阻止您以任何足以工作的黑客方式来实现它。

1You're always inheriting from object, whether you wrote it in the class declaration or not. Many open source codebases will inherit from object explicitly anyway in order to be cross-compatible with 2.7 runtimes.

2This point is explained in greater detail, along with the subtle relationship between __new__ and __init__, in CPython sources here https://github.com/python/cpython/blob/24bd50bdcc97d65130c07d6cd26085fd06c3e972/Objects/typeobject.c#L3598-L3638.

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当直接从`object`继承时,我应该调用super().__init__()吗? 的相关文章

随机推荐