本文整理自《Effective Python 编写高质量 Python 代码的 59 个有效方法》第 25条:用 super 初始化父类
简单的继承体系
初始化父类的传统方式是在子类里用子类实例直接调用父类的 __init__ 方法
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class MyChildClass(MyBaseClass):
def __init__(self):
MyBaseClass.__init__(self,5)
多重继承
多重基础基础例子
如果子类收到多重继承的影响(通常应该避免该做法),直接调用超类的 __init__ 方法会产生问题。
在子类里调用 __init__ 问题之一是调用顺序不固定,例如下面两个超类都操作了 value 实例字段,子类继承了各超类:
class MyBaseClass(object):
def __init__(self, value):
self.value = 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)
if __name__ == '__main__':
foo = OneWay(5)
print(f"顺序 1:(5 * 2)+ 5 = {foo.value}")
得到打印结果:
顺序 1:(5 * 2)+ 5 = 15
修改超类定义顺序
构建类实例后产生结果与继承超类顺序一致得到。但用另外一种顺序来定义它所继承各超类时:
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)
class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
def __init__(self, value):
MyBaseClass.__init__(self, value)
TimesTwo.__init__(self)
PlusFive.__init__(self)
if __name__ == '__main__':
foo = OneWay(5)
print(f"顺序 1:(5 * 2)+ 5 = {foo.value}")
bar = AnotherWay(5)
print(f"顺序 2:{bar.value}")
虽然我们调整了超类的定义顺序,但超类构造器的调用顺序没变,所以结果仍旧和之前相同,先调用 TimesTwo 再调用 PlusFive 的初始化,导致该结果与超类定义顺序不相符。
钻石形继承
若两个超类继承自相同公共基类,子类继承自这两个超类,这会使钻石顶部的公共基类多次执行 __init__ 产生意外问题。
class MyBaseClass(object):
def __init__(self, value):
self.value = value
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
class ThirdWay(TimesFive, PlusTwo):
def __init__(self, value):
TimesFive.__init__(self, value)
PlusTwo.__init__(self, value)
if __name__ == '__main__':
foo = ThirdWay(5)
print(f"顺序 3 计划(5 * 5)+ 2 = 27 然而实际值是 {foo.value}")
得到结果:
顺序 3 计划(5 * 5)+ 2 = 27 然而实际值是 7
这是因为在调用 PlusTwo.__init__ 第二个超类的构造器时,导致 self.value 重新变成 5。
super 函数
python 设置了内置的 super 函数,并且定义了方法解析顺序(Method Resolution Order,MRO)来解决超类之间的初始化顺序问题(例如深度优先、从左至右等),它也保证钻石顶部的公共基类的 __init__ 只会运行一次。
例如 Python 3 中改写刚刚的代码:
class MyBaseClass(object):
def __init__(self, value):
self.value = value
class TimesFive(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value *= 5
class PlusTwo(MyBaseClass):
def __init__(self, value):
super().__init__(value)
self.value += 2
class FourthWay(TimesFive, PlusTwo):
def __init__(self, value):
super().__init__(value)
if __name__ == '__main__':
foo = FourthWay(5)
print(f"顺序 4 计划(5 * 5)+ 2 = 27 然而实际值是 {foo.value}")
得到结果输出如下:
顺序 4 计划(5 * 5)+ 2 = 27 然而实际值是 35
按照设想,应该先运行 TimesFive._init__ 再运行 PlusTwo._init__ 得出结果 27,但实际却并非如此,这是由 FourthWay 类的 MRO 顺序决定的。
print(FourthWay.mro())
我们可以拿到 FourthWay 类的 MRO 顺序如下:
[
<class '__main__.FourthWay'>,
<class '__main__.TimesFive'>,
<class '__main__.PlusTwo'>,
<class '__main__.MyBaseClass'>,
<class 'object'>
]
在调用 FourthWay(5) 时,它会按上面列表顺序依次调用,在到达了钻石体系的顶部之后,所有的初始化方法会按照与刚才 __init__ 相反的顺序来运作。于是 MyBaseClass.__init__ 先把 value 设为 5,PlusTwo.__init__ 为它加 2,最后 TimesFive.__init__ 为它乘 5 得到 35。
Python 2 & 3 中区别
Python 2 中用特殊方法实现 super,语句写起来有些麻烦,必须制定当前所在的类和self对象,这种构造方式有些费解;调用 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
Python 3 中可以在方法中通过 __class__ 变量准确应用当前类,所以以下两种写法都能够正常运作,而 Python 2 中没有定义 __class__, 不能采用该写法。
要点小结
Python 采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题。
总是应该使用内置的 super 函数来初始化父类。