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

时间:2018-09-11 09:27:03

标签: python inheritance super

由于这个问题是关于继承和 Dim str As [String] = "" For i As Integer = 0 To CheckBoxList1.Items.Count - 1 If CheckBoxList1.Items(i).Selected Then If str = "" Then str = "'" + CheckBoxList1.Items(i).Value + "'" Else str += "," + "'" + CheckBoxList1.Items(i).Value + "'" End If End If Next 'display in a textbox or lable with ID txtmsg txtmsg.Text = str 的,所以让我们从编写一个类开始。这是一个代表人的简单日常课程:

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__()最终调用Person,由于缺少Horse.__init__参数而引发异常。

但是,如果fur_colorPerson没有呼叫Horse,就不会存在此问题。

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


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


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

3 个答案:

答案 0 :(得分:7)

如果从未将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__。从__init__继承来的PersonCentaur的MRO会做正确的事情,将fur_color传递给Horse.__init__

答案 1 :(得分:6)

  

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

您似乎在搜索该问题的简单“是或否”答案,但是不幸的是答案是“取决于”。此外,在决定是否应调用super().__init__()时,类是否直接继承自object有点无关紧要。不变的是,如果调用object.__init__,则应在不带参数的情况下调用它-因为object.__init__不接受参数。

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

我之前提到过,一个类是否直接从object继承是一个红色鲱鱼 1 。但是我还没有提到什么可以激发这个设计决定:如果您希望继续在MRO中搜索其他初始化程序,则应调用super init [read:super anymethod] [read:other {{1} } s]。如果要指示应该在此处停止MRO搜索,则不应调用super。

为什么anymethod根本不存在,为什么会存在?因为它确实做了一些事情:确保了它不带参数地调用。参数的存在可能表示错误 2 object.__init__的作用还在于停止超级调用链-某人不必调用超级,否则我们将无限递归。您可以通过不调用super来明确地自己停止它。如果没有,object将作为最终链接并为您停止链接。

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

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

在极少数情况下,您可能有条件地调用__init__调用。您可以检查您的super实例是否具有此属性或该属性,并根据结果确定一些逻辑。或者,您可以调用super()来显式“跳过”链接并手动遍历此部分的MRO。是的,如果默认行为不是您想要的,则可以劫持MRO!所有这些恶魔般的想法的共同点是对C3 linearization算法的理解,Python如何进行MRO以及super本身如何使用MRO。 Python的实现或多或少地从another programming language(其中super被命名为next-method)中解放出来。坦白地说,super(OtherClass, self)在Python中是一个超级坏名字,因为它引起初学者之间的普遍误解,即您总是向一个父类调用“ up”,我希望他们选择了一个更好的名字。

在定义继承层次结构时,解释器无法知道您是否要重用其他一些现有功能的类,还是要用替代实现将其替换? 。任何一个决定都可能是有效且实用的设计。如果在何时何地如何调用super上有一个严格的规定,那么程序员就不必选择它了-该语言将自动做出决定,并自动执行正确的操作。我希望能充分说明在super中调用super并不是一个简单的是/否问题。

  

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

(问题的this revisionSuperFooFoo等的来源)

为了回答这一部分,我将假定MCVE中显示的SuperFoo方法实际上需要进行一些初始化(也许您可以在问题的MCVE代码中添加占位符注释)。如果您唯一要做的就是使用相同的参数调用super,则根本不要定义__init__,这毫无意义。不要定义仅__init__的{​​{1}},除非您有意停止在那里的MRO遍历(在这种情况下,一定要发表评论!)。

首先,在我们讨论__init__之前,让我说pass看起来像是不完整的或不良的设计。您如何将SuperFoo参数传递给NoSuperFoo的init? foo的{​​{1}}初始值已硬编码。硬编码(或以其他方式自动确定)foo的初始值可能是可以的,但是可以you should probably be doing composition not inheritance

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

Foo

请注意,SuperCls 有一个 Foo,而不是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,并且可以在{{ 1}}否则会更容易-通过使用组合来避免违反LSP常常更容易。 class FooParent(Foo)还应通过允许FooParent进行合作:

Foo

也许Foo也超出了您的控制范围,您也必须对其进行调整,就这样吧。关键是,这是通过调整接口以使签名匹配来重用代码的方法。假设每个人都很好地合作并消费了他们所需的东西,那么SuperCls最终将代理**kwargs

  

由于我已经看到99%的类在其构造函数中未使用class SuperCls: def __init__(self, **kwargs): # some other init code here super().__init__(**kwargs) ,这是否意味着99%的python类实现不正确?

否,因为YAGNI。在99%的类有用之前,是否需要立即支持所有风吹草动的100%一般依赖注入?如果没有,它们会破裂吗?例如,请考虑集合文档中提供的OrderedCounter配方。 Counter.__init__接受SuperClssuper().__init__(**kwargs),但接受doesn't proxy them in the super init call。如果您想使用其中一个参数,祝您好运,那么必须重写object.__init__(**{})并截取它们。 OrderedDict根本没有被合作定义,实际上,某些parent calls are hardcoded**kwargs-并且未调用下一行中的任何内容的*args,因此任何MRO遍历都会停在那儿。如果您不小心将其定义为**kwargs而不是__init__,则元类基础仍然可以创建一致的MRO,但该类根本无法用作有序计数器。

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

1 无论您是否在类声明中编写对象,都始终从对象继承。无论如何,许多开源代码库都会从对象显式继承,以便与2.7运行时交叉兼容。

2 在CPython来源here中将更详细地说明这一点以及OrderedCounter(OrderedDict, Counter)OrderedCounter(Counter, OrderedDict)之间的微妙关系。 / sub>

答案 2 :(得分:-1)

使用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

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