在子类中初始化父类, 传统的方式是在子类中直接调用父类的__init__
函数:
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class MyChildClass(MyBaseClass):
def __init__(self):
MyBaseClass.__init__(self, 5)
在继承体系很简单的情况下, 这种方式可以正常运行, 但是在大多数情况下这种方法都是不可行的. 如果一个类继承自多个类, 那么直接调用父类的__init__
函数就会产生不可预知的结果.
问题之一是, 在子类里调用__init__
的顺序并不固定. 例如定义两个父类:
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) + 5 = 15
下面这个类以另外一种顺序继承两个父类:
class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
但是在构造函数中调用父类__init__
方法的顺序没有改变, 这就导致了该类所产生的结果与其继承顺序不一致:
bar = AnotherWay(5)
print('Second ordering still is', bar.value)
>>>
Second ordering still is 15
另一种问题出现在菱形继承(diamond inheritance)中. 菱形继承是指一个类继承自两个不同的类, 而这两个类有一个共同父类. 在菱形继承中, 公共父类的__init__
构造方法会运行多次, 从而导致意料之外的结果. 例如, 以下两个类都继承自MyBaseClass
:
class TimesFive(MyBaseClass):
def __init__(self, value):
MyBaseClass.__init__(self, value)
self.value *= 5
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
我们本来想要的结果是(5 * 5) + 2 = 27
, 但是调用第二个父类PlusTwo
的构造方法PlusTwo.__init__
时, 第二次调用了顶端父类MyBaseClass
的构造方法MyBaseClass.__init__
, 使得self.value
又变成了5
.
为了解决这个问题, Python2.2添加了一个名为super
的内置函数, 并定义了方法解析顺序(Method Resolution Order, MRO). MRO以标准的流程来安排超类之间的初始化顺序, 也保证了菱形继承中顶端类的__init__
方法只会运行一次.
以下代码使用Python2风格的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语句中的出现顺序相同:
# Python 2
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
我们可能会认为应该是先*5
, 然后+2
得出结果是27
才对. 但实际上, 程序的运行顺序和GoodWay
类的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(5)
时, 它会调用TimesFiveCorrect.__init__
, 而TimesFiveCorrect.__init__
又会调用PlusTwoCorrect.__init__
, 接着PlusTwoCorrect.__init__
调用MyBaseClass.__init__
, 到达菱形顶部后, 所有的构造方法会按照与刚才那些__init__
相反的顺序来执行, 于是MyBaseClass.__init__
会把value
设为5
, 然后PlusTwoCorrect.__init__
会为其加2, 变成7
, 最后TimesFiveCorrect.__init__
将其乘以5, 结果就变为35
.
在Python2中有这样两个问题:
-
super
语句有些麻烦, 必须指定所在的类,self
对象, 构造方法名(通常是__init__
)以及所有的参数值. -
调用
super
时, 必须写出当前类的名称, 但是类的名称很可能会更改, 那么每一条super
语句也就必须要更改.
在Python3中则没有这些问题, 它提供了一种不带参数的super
调用方式:
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
小结
-
Python使用MRO来解决父类初始化顺序及菱形继承问题;
-
应该使用内置的
super
函数来初始化父类.
参考资料:
Slatkin, Brett. Effective Python: 59 Specific Ways to Write Better Python. Pearson Education, 2015.
Brett Slatkin, 爱飞翔. Effective Python: 编写高质量Python代码的59个有效 方法. 机械工业出版社, 2016.1.