使用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的时候,程序并没有真正的执行,而是走到这条继承树的树根,从树根往下才会进行初始化。于是我们会先初始化MyBaseClassvalue5,然后是PlusTwoCorrectvalue会变成7,接着TimesFiveCorrectvalue就自然的变成35了。这也是为什么我们的最终结果是35的原因。这下,明白了是为什么了吧 :-)

两个明显问题

虽然Python2super工作的很好,但是我们还是需要注意两个很明显的问题。

  • 语法有点啰嗦,你不得不指定你所在的类,self对象,方法名(通常是__init__),以及所有的参数。对于Python初学者而言,这无疑是很让人困惑的。
  • 在调用super方法的时候,你不得不需要通过名字来指定当前类。如果你曾经改变过类的名称(在改善分层架构的时候的一个很常见的行为),你需要手动的更新所有使用到了super的地方的代码。

谢天谢地,Python3修复了这些问题。原理是在调用super关键字的时候不使用参数,等价于使用了带有__class__和指定了selfsuper函数。在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来初始化父类。

results matching ""

    No results matching ""