当直接从“object”继承时,是否应该调用super()。\uu init_Uu()?

2024-09-24 02:18:57 发布

您现在位置:Python中文网/ 问答频道 /正文

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

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

        self.name = name

就像每一个好的类一样,它在初始化自身之前调用其父构造函数。这个类的工作做得很好;它可以毫无问题地使用:

^{pr2}$

但是当我试图创建一个同时继承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

由于diamond继承(顶部是object类),因此无法正确初始化Centaur实例。Person中的super().__init__()最后调用了Horse.__init__,这将引发异常,因为缺少fur_color参数。在

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

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


免责声明:我知道what ^{} doesMRO是如何工作的,以及{a3}。我知道是什么导致了这个错误。我只是不知道怎样才能避免错误。在


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


Tags: nameselfobjectinitdefwhatclasscolor
3条回答

如果Person和{}从来没有被设计成用作同一类的基类,那么{}可能不应该存在。正确地设计多重继承是非常困难的,远不止调用super。即使是单一的继承也是相当棘手的。在

如果PersonHorse支持创建类的,那么Person和{}(可能还有它们周围的类)需要重新设计。从这里开始:

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")

你会注意到一些变化。让我们看看名单。在

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

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

第三,Centaur不再有自己的__init__。重新设计了Person和{},它不需要__init__。从Person继承的__init__将正确处理Centaur的MRO,将fur_color传递给Horse.__init__。在

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

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

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

^{pr2}$

输出为:

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

这显示了调用构造函数的顺序。还要注意,子类构造函数都有兼容的签名,因为它们是在心里彼此编写的。在

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

你似乎在为这个问题寻找一个简单的“是或否”答案,但不幸的是,答案是“视情况而定”。此外,在决定是否应该调用super().__init__()时,类是否直接从object继承有些无关紧要。不变的是,如果调用了object.__init__,那么应该不带参数地调用它,因为object.__init__不接受参数。在

实际上,在协作继承的情况下,这意味着您必须确保在调用object.__init__之前,所有参数都被使用。它并不意味着您应该避免调用object.__init__Here是在调用super之前使用参数的一个例子,response和{}上下文已从可变映射^{中弹出。在

我在前面提到过,一个类是否直接从object继承是一个红鲱鱼1。但是我还没有提到什么应该激励这个设计决策:如果你想让MRO继续搜索其他初始化器[read:otheranymethod],那么应该调用super init[read:superanymethod]。如果您希望在此处停止MRO搜索,则不应调用super。在

如果object.__init__不起任何作用,为什么它会存在呢?因为它做了一些事情:确保调用它时没有参数。参数的存在很可能表示bug2object还可以用来停止超级调用链-某人不必调用super,否则我们将无限递归。您可以通过不调用super直接自己停止它。如果不这样做,object将作为最后一个链接,并为您停止链。在

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

  • 调用super方法时使用哪些参数(例如,您希望沿着MRO转发哪些参数)
  • 是否调用super(有时您希望故意断开链)
  • 创建super本身的参数(如果有)(下面描述了一个高级用例)
  • 在你自己的方法中,是在之前还是在之后调用一个代理方法(如果需要访问被代理方法设置的某些状态,请将super放在第一位;如果你正在设置代理方法依赖的某个状态,那么就把super放在最后一个位置——在某些情况下,你甚至想把super放在中间的某个地方!)在
  • 这个问题主要是关于__init__,但是不要忘记super也可以与任何其他方法一起使用

在极少数情况下,您可能会有条件地调用super调用。您可以检查您的super()实例是否具有this或that属性,并根据结果建立一些逻辑。或者,您可以调用super(OtherClass, self)显式地“跳过”一个链接,并手动遍历该部分的MRO。是的,如果默认行为不是你想要的,你可以劫持MRO!所有这些邪恶思想的共同点是对C3 linearization算法的理解,Python如何生成MRO,以及super本身如何使用MRO。Python的实现或多或少是从another programming language中提取出来的,其中super被命名为^{}。老实说,super在Python中是一个非常糟糕的名字,因为它在初学者中造成了一个常见的误解,即您总是调用“最多”一个父类,我希望他们选择了一个更好的名称。在

在定义继承层次结构时,解释器无法知道您是否希望重用某些其他类的现有功能,还是将其替换为替代实现,或者其他什么。任何一个决定都可能是一个有效和实用的设计。如果有一个关于何时和如何super的硬性规定当被调用时,它不会由程序员来选择——语言将从你的手中夺走决定权,只是自动地做正确的事情。我希望这能充分解释在__init__中调用super不是一个简单的yes/no问题。在

If yes, how would you correctly initialize SuperFoo?

(问题FooSuperFoo等的来源)

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

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

至于SuperFoo,它继承了SuperCls和{}。SuperCls看起来是为了继承,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,而不是{}是一个Foo。这不是唯一可能的设计选择。但是,如果您像class FooParent(Foo)那样继承,那么您就意味着FooParent是一个Foo,并且可以用于Foo的任何地方,使用组合通常更容易避免违反LSPSuperCls还应通过允许{}进行合作:

^{pr2}$

也许{}也超出了你的控制范围,你也必须适应它,所以就这样吧。关键是,这是一种重用代码的方法,通过调整接口使签名匹配。假设每个人都很好地合作并消费他们所需要的,最终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。99%的类在有用之前,是否需要立即支持100%的常规依赖注入?如果它们不坏的话,是不是坏了?作为一个例子,考虑集合文档中给出的^{}配方。^{}接受*args和{},但是{a13}。如果你想使用这些参数中的一个,运气不好,你必须重写__init__并截获它们。^{}根本没有协同定义,实际上,一些parent calls are hardcoded到{}-并且任何下一行的__init__都不会被调用,因此任何MRO遍历都将在其轨道上停止。如果您意外地将其定义为OrderedCounter(OrderedDict, Counter)而不是OrderedCounter(Counter, OrderedDict),元类基仍然能够创建一致的MRO,但是该类根本不能作为有序计数器工作。在

尽管有这些缺点,但是OrderedCounter配方还是像宣传的那样工作,因为MRO是为预期的用例设计的。因此,为了实现依赖注入,您甚至不需要100%正确地进行协作继承。这个故事的寓意是完美是进步的敌人。如果你想把MyWhateverClass塞进你能想象得到的任何疯狂的继承树中,那就继续吧,但这是由你自己来编写必要的脚手架来实现的。与往常一样,Python不会阻止您以任何足够好的方法来实现它。在

1无论是否在类声明中编写,都始终从object继承。为了与2.7运行时交叉兼容,许多开源代码库都会显式地从object继承。

2这一点将与sub一起进行更详细的解释CPython源here__new__和{}之间的关系。

相关问题 更多 >