使用super关键字初始化父类
多继承
较早的在子类中初始化父类的方法是在子类实例中通过直接调用父类的__init__
来实现。
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class MyChildClass(MyBaseClass):
def __init__(self):
MyBaseClass.__init__(self, 5)
对于简单分层的类树而言,这种方式直截了当,简单粗暴,但是在许多场合下就实效甚至崩溃了。
如果你的类受多继承的影响(通常情况下要尽量的避免,详见第26
项:只在Mix-in
情况下使用多继承),直接地调用超累的__init__
方法可能会导致意想不到的行为发生。
一个问题是__init__
的调用顺序不能在子类中被明确。例如:这里我定义了两个父类,都有一个value
字段,且初始化的值不相同,这个时候多继承就有可能出现问题了。
class TimesTwo(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 ) + 2 = 15
下面使用不同的次序来初始化一个新子类。
class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
但是,接下来的事情会让我们感到十分的惊讶。代码中我没有改变父类初始化的顺序,和上面的那个例子保持了一致,从而会引起和多继承顺序不匹配的结果值。如下:
bar = AnotherWay(5)
print("Second ordering still is", bar.value)
>>>
Second ordering still is 15
菱形继承就很容易发生类似的问题。而什么是菱形继承呢?说白了就是孙子类的两个爸爸的父类指向了同一个基类,共用了一个爷爷类。在__init__
方法运行多次的时候,菱形继承很容易出现这样的让人意想不到的问题。这里,我定义了两个继承与MyBaseClass
的子类。
class TimesFive(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 5
class PlueTwo(MyBaseClass):
def __init__(self, value):
MyBaseClas.__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
的结果却变成了7呢?便是因为第二个子类在调用MyBaseClass.__init__
方法的时候把value
重置为了5
,所以出现了最后为7
的结果。
为了解决这个问题,Python2.2
增加了一个super的内置函数,并且定义了MRO(Method Resolution Order)
。MRO
将父类的初始化顺序进行了标准化的声明。也保证了菱形继承中超类(MyBaseClass
)只执行一次。这里,我又创建了一个菱形继承的例子,但是这次用super
(Python2
风格)来初始化父类。
# Python 2
class TimesFiveCorrect(MyBaseClass):
def __init__(self, value):
super(TimesFiveCorrect, slef).__init__(value)
self.value *= 5
class PlusTwoCorrect(MyBaseClass):
def __init__(self, value):
super(PlusTwoCorrect, self).__init__(value)
self.value += 2
# 现在,菱形继承的超类,也就是最顶上的那个`MyBaseClass`只会被初始化一次,而其他的两个父类会按照被声明的顺序来初始化了。
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
这个顺序看起来可能一开始很不好。难道不应该TimesFiveCorrect.__init__
方法第一次执行吗?结果不应该是(5 * 5 ) + 2 = 27
的吗?
答案是不是这样的。父类实例化的规则是按照MRO
标准来进行的。而一个类的MRO
顺序可以通过调用mro
函数来获悉。
from pprint import pprint
pprint(GoodWay.mro())
>>>
[<class '__main__.GoodWay'>,
<class '__main__.TimesFiveCorrect'>,
<class '__main__.PlusTwoCorrect'>,
<class '__main__.MyBaseClass'>,
<class 'object'>]
这样就很容易可以理解了吧。最开始初始化GoodWay
的时候,程序并没有真正的执行,而是走到这条继承树的树根,从树根往下才会进行初始化。于是我们会先初始化MyBaseClass
的value
为5
,然后是PlusTwoCorrect
的value
会变成7
,接着TimesFiveCorrect
的value
就自然的变成35
了。这也是为什么我们的最终结果是35
的原因。这下,明白了是为什么了吧 :-)
两个明显问题
虽然Python2
中super
工作的很好,但是我们还是需要注意两个很明显的问题。
- 语法有点啰嗦,你不得不指定你所在的类,
self
对象,方法名(通常是__init__
),以及所有的参数。对于Python
初学者而言,这无疑是很让人困惑的。 - 在调用
super
方法的时候,你不得不需要通过名字来指定当前类。如果你曾经改变过类的名称(在改善分层架构的时候的一个很常见的行为),你需要手动的更新所有使用到了super
的地方的代码。
谢天谢地,Python3
修复了这些问题。原理是在调用super
关键字的时候不使用参数,等价于使用了带有__class__
和指定了self
的super
函数。在Python3
中,你总是应该使用super
关键字,就是因为它的简洁性,明智性以及总是做对的事情这些特点。
# Python 3
class Explicity(MyBaseClass):
def __init__(self, value):
super(__class__, self).__init__(values * 2)
class Implicit(MyBaseClass):
def __init__(self, value):
super().__init__(value * 2)
assert Expilcit(10).value == Implicit(10).value
代码可以工作的前提就是Python
允许你使用__class__
来引用当前类。而在Python2
中是不能支持的。你可能猜想在Python3
中,你可以使用self.__class__
来作为一个参数传递给super
,但是事实却会让你大跌眼镜,因为这样的super
只在Python2
中实现了。
备忘录
Python
的解决实例化次序问题的方法MRO
解决了菱形继承中超类多次被初始化的问题。- 总是应该使用
super
来初始化父类。