30.5.4. 通过super初始化超类

以前有种简单的写法,能在子类里面执行超类的初始化逻辑,那就是直接在超类名称上调用__init__方法并把子类实例传进去。

class MyBaseClass:
    def __init__(self, value):
        self.value = value


class MyChildClass(MyBaseClass):
    def __init__(self):
        MyBaseClass.__init__(self, 5)

直接调用__init__方法所产生的第一个问题在于,超类的构造逻辑不一定会按照它们在子类class语句中的声明顺序执行。例如,在MyBaseClass之外再定义两个类,让它们也分别去操纵本实例的value字段。

下面这个子类继承了刚才那三个类,而且它在class语句里指定的超类顺序与它执行那些超类的__init__时所用的顺序一致。

class PlusFive:
    def __init__(self):
        self.value += 5


class OneWay(MyBaseClass, PlusFive, TimesTwo):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        TimesTwo.__init__(self)
        PlusFive.__init__(self)


foo = OneWay(5)
print("First ordering value is (5 * 2) +5 =", foo.value)
print("Second ordering value is ", foo.value)

#First ordering value is (5 * 2) +5 = 15
#Second ordering value is  15

直接调用__init__所产生的第二个问题在于,无法正确处理菱形继承(diamondinheritance)。这种继承指的是子类通过类体系里两条不同路径的类继承了同一个超类。如果采用刚才那种常见的写法来调用超类的__init__,那么会让超类的初始化逻辑重复执行,从而引发混乱。例如,下面先从MyBaseClass派生出两个子类。

当ThisWay调用第二个超类的__init__时,那个方法会再度触发MyBaseClass的__init__,导致self.value重新变成5。所以,最后的结果是5 + 9 = 14,而不是(5 * 7) + 9 = 44,因为早前由TimesSeven.__init__所做的初始化效果已经被第二次执行的MyBaseClass.__init__覆盖了。这是个违背直觉的结果,如果情况更为复杂,那么调试起来会特别困难。

class TimesSeven(MyBaseClass):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        self.value *= 7


class PlusNIne(MyBaseClass):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        self.value += 9


class ThisWay(TimesSeven, PlusNIne):
    def __init__(self, value):
        TimesSeven.__init__(self, value)
        PlusNIne.__init__(self, value)


foo = ThisWay(5)
print("Should be (5*7) +9 =44 but is ", foo.value)

#Should be (5*7) +9 =44 but is  14

为了解决这些问题,Python内置了super函数并且规定了标准的方法解析顺序(method resolution order,MRO)。super能够确保菱形继承体系中的共同超类只初始化一次。

下面再创建一套菱形的类体系,但是这次,我们改用super()来调用超类的初始化逻辑。

位于菱形结构顶端的MyBaseClass,会率先初始化,而且只会初始化一次。接下来,程序会参照菱形底端那个子类在class语句里声明超类时的顺序,来执行菱形结构中部的那两个超类。

class TimesSevenCorrect(MyBaseClass):
    def __init__(self, value):
        # super().__init__(value)       #等同于下面方式
        # super(__class__, self).__init__(value)  # 等同于下面方式
        super(TimesSevenCorrect, self).__init__(value)
        self.value *= 7


class PlusNIneCorrect(MyBaseClass):
    def __init__(self, value):
        # super().__init__(value)       #等同于下面方式
        # super(__class__, self).__init__(value)  # 等同于下面方式
        super(PlusNIneCorrect, self).__init__(value)
        self.value += 9


class GoodWay(TimesSevenCorrect, PlusNIneCorrect):
    def __init__(self, value):
        # super().__init__(value)       #等同于下面方式
        # super(__class__, self).__init__(value)  # 等同于下面方式
        super(GoodWay, self).__init__(value)


foo = GoodWay(5)
mro_str = "\n ".join(repr(cls) for cls in GoodWay.mro())
print(mro_str)
print("Should be (5*7) +9 =44 and is ", foo.value)
'''
<class '__main__.GoodWay'>
 <class '__main__.PlusNIneCorrect'>
 <class '__main__.TimesSevenCorrect'>
 <class '__main__.MyBaseClass'>
 <class 'object'>
Should be (5*7) +9 =44 and is  44
'''

调用GoodWay(5)时,会先触发TimesSevenCorrect.__init__,进而触发PlusNine-Correct.__init__,而这又会触发MyBaseClass.__init__。程序到达菱形结构的顶端后,开始执行MyBaseClass的初始化逻辑,然后按照与刚才相反的顺序,依次执行PlusNineCorrect、TimesSevenCorrect与GoodWay的初始化逻辑。所以,程序首先会在MyBaseClass.__init__中,把value设为5,然后在PlusNineCorrect.__init__里面给它加9,这样就成了14,接着又会在TimesSevenCorrect.__init__里面将它乘7,于是等于98。

只有一种情况需要明确给super指定参数,这就是:我们想从子类里面访问超类对某项功能所做的实现方案,而那种方案可能已经被子类覆盖掉了(例如,在封装或复用功能时,就会遇到这样的情况)。

要点:

  • Python有标准的方法解析顺序(MRO)规则,可以用来判定超类之间的初始化顺序,并解决菱形继承问题。

  • 可以通过Python内置的super函数正确触发超类的__init__逻辑。一般情况下,不需要给这个函数指定参数。