初始化父类的传统方式,是在子类里用子类实例直接调用父类的 __init__
方法。
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class MyChildClass(MyBaseClass):
def __init__(self):
MyBaseClass.__init__(self, 5)
这种办法对于简单的继承体系是可行的,但是在这么多情况下会出问题。
如果子类受到了多重继承的影响(通常应该避免这种做法,请参见该书第26条),那么直接调用超类的 __init__
方法,可能会产生无法预知的行为。
在子类里调用 __init__
的问题之一,是它的调用顺序并不固定。例如,下面定义两个超类,它们都操作名为 value 的实例字段:
class TimeTwo(object):
def __init__(self):
self.value *= 2
class PlusFive(object):
def __init__(self):
self.value += 5
下面这个类用其中一种顺序来定义它所继承的各个超类。
class OneWay(MyBaseClass, TimesTwo, PlusFive):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
构建该类实例之后,我们发现,它所产生的结果与继承时的超类顺序相符。
foo = OneWay(5)
print('First ordering is (5 * 2) + 5 =', foo.value)
>>>
First ordering is (5 * 2) + 5 = 15
下面这个类,用另外一种顺序来定义它所继承的各个超类:
class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
但是,上面这段代码并没有修改超类构造器的调用顺序,它还是和以前一样,先调用 TimesTwo.__init__
,然后才调用 PlusFive.__init__
, 这就导致该类所产生的结果与其超类的定义顺序不相符。
bar = AnotherWay(5)
print('Second ordering still is', bar.value)
>>>
Second ordering still is 15
还有一个问题发生在钻石形继承之中。如果子类继承自两个单独的超类,而那两个超类又继承自同一个公共基类,那么就构成了钻石形继承体系。这种继承会使钻石顶部的那个公共基类多次执行 __init__
方法,从而产生意想不到的行为。例如,下面定义的这两个子类,都继承自 MyBaseClass。
class TimesFive(MyBaseClass):
def __init__(self):
self.value *= 2
class PlusTwo(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self,value)
self.value += 2
然后定义一个子类,同时继承上面这两个类,这样MyBaseClass 就成了铁钻石顶部的那个公共基类。
class ThisWay(TimesFive, PlusTwo):
def __init__(self, value):
TimesFive.__init__(self, value)
PlusTwo.__init__(self, value)
foo = ThisWay(5)
print('Should be (5*5)+2 = 27 but is', foo.value)
>>>
Should be (5*5)+2 = 27 but is 7
我们可能认为输出的结果会是27,因为 (5*5)+2=27,但实际上却是7,因为我们在调用第二个超类的构造器,也就是 PlusTwo.__init__
的时候,它会再度调用 MyBaseClass.__init__
,从而导致self.value 重新 变成5。
python2.2 增加了内置的super函数,并且定义了方法解析顺序(method resolution order, MRO),以解决这一问题。MRO以标准的流程来安排超类之间的初始化顺序(例如,深度优先、从左至右),它也保证钻石顶部那个公共基类的 __init__
方法只会支行一次。
下面重新创建钻石形的继承体系,但是这一次,我们用super初始化超类(范例代码以python 2 的风格来使用super):
# python2
class TimesFiveCorrect(MyBaseClass):
def __init__(self, value):
super(TimesFiveCorrect, self).__init__(value)
self.value *= 5
class PlusTwoCorrect(MyBaseClass):
def __init__(self,value):
super(PlusTwoCorrect, self).__init__(value)
self.value += 2
现在,对于处在钻石顶部的那个MyBaseClass 类来说,它的 __init__
方法只会运行一次。而其他超类的初始化顺序,则与这这些超类在class语句中出现的顺序相同。
class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
def __init__(self,value):
super(GoodWay, self).__init__(value)
foo = GoodWay(5)
print('should be 5*(5+2) =35 and is ',foo.value)
>>>
should be 5*(5+2) =35 and is 35
看到上面的运行结果之后,我们可能觉得程序的计算顺序和自己所想的刚好相——反。 应该先运行 -imesFiveCorrect.__init__
,然后运行 PlusTwoCorrect.__init__
,并得出 (5*5)+2=27 才对啊。但实际上却不是这样的。程序的运行顺序会与GoodWay类的MRO保持一致,这个MRO顺序可以通过名为mro的类方法来查询。
pprint(GoodWay.mro())
>>>
[<class '__main__.GoodWay'>,
<class '__main__.TimesFiveCorrect'>,
<class '__main__.PlusTwoCorrect'>,
<class '__main__.MyBaseClass'>,
<class 'object'>]
调用 GoodWay(5) 的时候,它会调用 TimesFiveCorrect.init,而 TimesFiveCorrect.__init__
又会调用 PlusTwoCorrect.__init__
, PlusTwoCorrect.__init__
会调用 MyBaseClass.__init__
。到达了钻石体系顶部之后,所有的初始化方法会按照与刚才那些 __init__
相反的顺序来运作。于是,MyBaseClass.__init__
会先把value 设为5,然后 PlusTwoCorrect.__init__
会为它加2,使value变成7,最后, TimesFiveCorrect.__init__
会将value乘以5, 使变为35。
python3提供了一种不带参数的super调用方式,该方式的效果与__class__
和self来调用super相同 。Python3 总是可以通过super写出清晰、精练而又准确的代码 。
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class Explicit(MyBaseClass):
def __init__(self, value):
super(__class__, self).__init__(value*2)
class Implicit(MyBaseClass):
def __init__(self,value):
super().__init__(value*2)
assert Explicit(10).value == Implicit(10).value
由于python3程序可以在方法中通过 __class__
变量准确地引用当前类,所以上面这种写法能够正常动作,而python2则没有定义__class__
,故而能采用这种写法。你可能想试着用self.__class__
做参数来调用super,但实际上这么做不行,因为python2是用特殊方式来实现super的。
要点:
- python采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题。
- 总是应该使用内置的super函数来初始化父类。