问题:用多重继承调用父类__init__,正确的方法是什么?
假设我有多个继承方案:
class A(object):
# code for A here
class B(object):
# code for B here
class C(A, B):
def __init__(self):
# What's the right code to write here to ensure
# A.__init__ and B.__init__ get called?
有编写的两个典型方法C
的__init__
:
- (老式)
ParentClass.__init__(self)
- (较新的样式)
super(DerivedClass, self).__init__()
但是,在任何一种情况下,如果父类(A
和B
)没有遵循相同的约定,则代码将无法正常工作(某些代码可能会丢失或多次调用)。
那么又是什么正确的方法呢?说“保持一致,遵循一个或另一个”很容易,但是如果A
或B
来自第三方图书馆,那又如何呢?有没有一种方法可以确保所有父类构造函数都被调用(以正确的顺序,并且只能调用一次)?
编辑:看看我的意思,如果我这样做:
class A(object):
def __init__(self):
print("Entering A")
super(A, self).__init__()
print("Leaving A")
class B(object):
def __init__(self):
print("Entering B")
super(B, self).__init__()
print("Leaving B")
class C(A, B):
def __init__(self):
print("Entering C")
A.__init__(self)
B.__init__(self)
print("Leaving C")
然后我得到:
Entering C
Entering A
Entering B
Leaving B
Leaving A
Entering B
Leaving B
Leaving C
请注意,B
init会被调用两次。如果我做:
class A(object):
def __init__(self):
print("Entering A")
print("Leaving A")
class B(object):
def __init__(self):
print("Entering B")
super(B, self).__init__()
print("Leaving B")
class C(A, B):
def __init__(self):
print("Entering C")
super(C, self).__init__()
print("Leaving C")
然后我得到:
Entering C
Entering A
Leaving A
Leaving C
请注意,B
永远不会调用init。因此,似乎除非我知道/控制我从(A
和B
)继承的类的初始化,否则我无法对正在编写的类(C
)做出安全选择。
回答 0
两种方式都可以正常工作。使用该方法super()
可为子类带来更大的灵活性。
在直接呼叫方式中,C.__init__
可以同时呼叫A.__init__
和B.__init__
。
使用时super()
,需要将类设计为在其中C
调用的协作式多重继承super
,这将调用A
的代码,该代码还将super
调用B
的代码。请参阅http://rhettinger.wordpress.com/2011/05/26/super-considered-super,以详细了解可以使用进行的操作super
。
[回答问题,稍后编辑]
因此,似乎除非我知道/控制我从(A和B)继承的类的初始化,否则我无法对我正在编写的类(C)做出安全的选择。
参考的文章显示了如何通过在A
和周围添加包装器类来处理这种情况B
。标题为“如何合并非合作类”的部分提供了一个可行的示例。
可能希望多重继承更容易,让您轻松组成Car和Airplane类来获得FlyingCar,但现实情况是,单独设计的组件通常需要适配器或包装器,然后才能像我们希望的那样无缝地组装在一起:-)
另一个想法:如果您对使用多重继承来编写功能不满意,则可以使用composition来完全控制在哪些情况下调用哪种方法。
回答 1
您问题的答案取决于一个非常重要的方面:您的基类是否设计用于多重继承?
有3种不同的方案:
基类是不相关的独立类。
如果您的基类是能够独立运行的独立实体,并且彼此之间不认识,则它们不是为多重继承而设计的。例:
class Foo: def __init__(self): self.foo = 'foo' class Bar: def __init__(self, bar): self.bar = bar
重要:请注意,既不打电话
Foo
也不Bar
打电话super().__init__()
!这就是为什么您的代码无法正常工作的原因。由于Diamond继承在python中的工作方式,因此object
不应调用基类为的类super().__init__()
。如您所知,这样做会破坏多重继承,因为您最终将调用另一个类的__init__
而不是object.__init__()
。(免责声明:避免super().__init__()
在object
-subclasses中是我个人的建议,绝不是python社区中达成一致的共识。有些人更喜欢super
在每个类中使用,认为如果该类的行为不像您通常可以编写一个适配器您期望的。)这也意味着您永远不应编写从其继承
object
且没有__init__
方法的类。完全不定义__init__
方法与调用具有相同的效果super().__init__()
。如果您的类直接继承自object
,请确保添加一个空的构造函数,如下所示:class Base(object): def __init__(self): pass
无论如何,在这种情况下,您将必须手动调用每个父构造函数。有两种方法可以做到这一点:
不带
super
class FooBar(Foo, Bar): def __init__(self, bar='bar'): Foo.__init__(self) # explicit calls without super Bar.__init__(self, bar)
用
super
class FooBar(Foo, Bar): def __init__(self, bar='bar'): super().__init__() # this calls all constructors up to Foo super(Foo, self).__init__(bar) # this calls all constructors after Foo up # to Bar
这两种方法各有其优点和缺点。如果你使用
super
,你的类将支持依赖注入。另一方面,容易出错。例如,如果你改变的顺序Foo
和Bar
(像class FooBar(Bar, Foo)
),你就必须更新super
到匹配的电话。没有super
您,不必担心这一点,并且代码更具可读性。类之一是mixin。
一混入是,这是一个一流的设计与多重继承使用。这意味着我们不必手动调用两个父构造函数,因为mixin会自动为我们调用第二个构造函数。由于这次只需要调用一个构造函数,因此
super
可以避免对父类的名称进行硬编码。例:
class FooMixin: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # forwards all unused arguments self.foo = 'foo' class Bar: def __init__(self, bar): self.bar = bar class FooBar(FooMixin, Bar): def __init__(self, bar='bar'): super().__init__(bar) # a single call is enough to invoke # all parent constructors # NOTE: `FooMixin.__init__(self, bar)` would also work, but isn't # recommended because we don't want to hard-code the parent class.
这里的重要细节是:
- mixin调用
super().__init__()
并通过它接收的任何参数。 - 子类首先从mixin继承:
class FooBar(FooMixin, Bar)
。如果基类的顺序错误,则将永远不会调用mixin的构造函数。
- mixin调用
所有基类均设计用于协作继承。
专为合作继承而设计的类非常类似于mixin:它们将所有未使用的参数传递给下一类。和以前一样,我们只需要调用即可
super().__init__()
,所有父级构造函数都将被链调用。例:
class CoopFoo: def __init__(self, **kwargs): super().__init__(**kwargs) # forwards all unused arguments self.foo = 'foo' class CoopBar: def __init__(self, bar, **kwargs): super().__init__(**kwargs) # forwards all unused arguments self.bar = bar class CoopFooBar(CoopFoo, CoopBar): def __init__(self, bar='bar'): super().__init__(bar=bar) # pass all arguments on as keyword # arguments to avoid problems with # positional arguments and the order # of the parent classes
在这种情况下,父类的顺序无关紧要。我们
CoopBar
最好还是从头继承,而代码仍然可以正常工作。但这是真的,因为所有参数都作为关键字参数传递。使用位置参数将很容易弄错参数的顺序,因此,协作类习惯于仅接受关键字参数。这也是我前面提到的规则的一个exceptions:
CoopFoo
和CoopBar
都继承自object
,但它们仍然调用super().__init__()
。如果没有,则不会有合作继承。
底线:正确的实现取决于您从其继承的类。
构造函数是类的公共接口的一部分。如果该类被设计为混合或协作继承,则必须将其记录下来。如果文档中未提及任何内容,则可以安全地假定该类不是为协作多重继承而设计的。
回答 2
这两种方法(“新风格”或“旧式”),将工作,如果你有过的源代码控制A
和B
。否则,可能需要使用适配器类。
可访问的源代码:正确使用“新样式”
class A(object):
def __init__(self):
print("-> A")
super(A, self).__init__()
print("<- A")
class B(object):
def __init__(self):
print("-> B")
super(B, self).__init__()
print("<- B")
class C(A, B):
def __init__(self):
print("-> C")
# Use super here, instead of explicit calls to __init__
super(C, self).__init__()
print("<- C")
>>> C()
-> C
-> A
-> B
<- B
<- A
<- C
在此,方法解析顺序(MRO)规定以下内容:
C(A, B)
A
首先决定,然后B
。MRO是C -> A -> B -> object
。super(A, self).__init__()
沿始于的MRO链继续C.__init__
进行B.__init__
。super(B, self).__init__()
沿始于的MRO链继续C.__init__
进行object.__init__
。
您可以说这种情况是为多重继承而设计的。
可访问的源代码:正确使用“旧样式”
class A(object):
def __init__(self):
print("-> A")
print("<- A")
class B(object):
def __init__(self):
print("-> B")
# Don't use super here.
print("<- B")
class C(A, B):
def __init__(self):
print("-> C")
A.__init__(self)
B.__init__(self)
print("<- C")
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C
在此,MRO无关紧要,因为A.__init__
和B.__init__
被显式调用。class C(B, A):
也会一样工作。
尽管这种情况不是像以前的样式那样“设计”为新样式的多重继承,但多重继承仍然是可能的。
现在,如果A
和B
是从第三方库-即你有过的源代码没有控制A
和B
?简短的答案:您必须设计一个实现必要super
调用的适配器类,然后使用一个空类来定义MRO(请参阅Raymond Hettinger上的文章super
-尤其是“如何合并非合作类”一节)。
第三方家长:A
未实施super
;B
确实
class A(object):
def __init__(self):
print("-> A")
print("<- A")
class B(object):
def __init__(self):
print("-> B")
super(B, self).__init__()
print("<- B")
class Adapter(object):
def __init__(self):
print("-> C")
A.__init__(self)
super(Adapter, self).__init__()
print("<- C")
class C(Adapter, B):
pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C
类Adapter
实现super
是为了C
定义MRO,该MRO在super(Adapter, self).__init__()
执行时起作用。
如果反过来呢?
第三方父母:A
工具super
;B
才不是
class A(object):
def __init__(self):
print("-> A")
super(A, self).__init__()
print("<- A")
class B(object):
def __init__(self):
print("-> B")
print("<- B")
class Adapter(object):
def __init__(self):
print("-> C")
super(Adapter, self).__init__()
B.__init__(self)
print("<- C")
class C(Adapter, A):
pass
>>> C()
-> C
-> A
<- A
-> B
<- B
<- C
此处的模式相同,除了执行顺序已切换Adapter.__init__
;super
先呼叫,然后再进行显式呼叫。请注意,带有第三方父母的每种情况都需要一个唯一的适配器类。
因此,似乎除非我知道/控制我从(
A
和B
)继承的类的初始化,否则我无法对正在编写的类(C
)做出安全选择。
虽然你可以处理,你没有的情况下,控制的源代码A
,并B
通过使用适配器类,这是事实,你必须知道在init怎样的父类实现super
(如果有的话),以这样做。
回答 3
正如雷蒙德(Raymond)在回答中所说的那样,直接调用A.__init__
并B.__init__
可以正常工作,并且您的代码易于阅读。
但是,它不使用C
和这些类之间的继承链接。利用该链接可为您提供更多的一致性,并使最终的重构更加容易且不易出错。如何执行此操作的示例:
class C(A, B):
def __init__(self):
print("entering c")
for base_class in C.__bases__: # (A, B)
base_class.__init__(self)
print("leaving c")
回答 4
本文有助于解释协作式多重继承:
http://www.artima.com/weblogs/viewpost.jsp?thread=281127
它提到了有用的方法mro()
,可向您显示方法解析顺序。在你的第二个例子,当你调用super
中A
,该super
呼叫继续在MRO。顺序中的下一个类是B
,这就是为什么B
init首次被调用的原因。
这是来自python官方站点的更多技术文章:
回答 5
如果要从第三方库中繁衍子类类,则不会,没有盲目的方法来调用__init__
实际上起作用的基类方法(或任何其他方法),而不管基类的编程方式如何。
super
使编写旨在协作实现方法的类成为复杂的多重继承树的一部分成为可能,而类继承者不必知道。但是无法使用它正确地从可能使用或可能不使用的任意类中继承super
。
本质上,一个类是设计为使用super
基类还是直接调用基类来进行子类化,是属于该类“公共接口”一部分的属性,因此应进行记录。如果您以库作者所期望的方式使用第三方库,并且库具有合理的文档,则通常会告诉您需要做什么来对特定的事物进行子类化。如果不是,那么您必须查看要子类化的类的源代码,并查看其基类调用约定是什么。如果你是从一个或多个第三方库的方式,该库作者结合多个类没想到,那么它可能无法始终如一地调用超类的方法在所有; 如果类A是使用的层次结构的一部分,super
而类B是不使用super的层次结构的一部分,则不能保证这两种选择都不会起作用。您将必须找出一种适用于每个特定案例的策略。