Python super 父类初始化

本文整理自《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 函数来初始化父类。


版权声明:本文为weixin_40796925原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。