每日10行代码126: 编写高质量python代码的方法25:用super 初始化父类

初始化父类的传统方式,是在子类里用子类实例直接调用父类的 __init__ 方法。

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

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

这种办法对于简单的继承体系是可行的,但是在这么多情况下会出问题。
如果子类受到了多重继承的影响(通常应该避免这种做法,请参见该书第26条),那么直接调用超类的 __init__方法,可能会产生无法预知的行为。
在子类里调用 __init__ 的问题之一,是它的调用顺序并不固定。例如,下面定义两个超类,它们都操作名为 value 的实例字段:

class TimeTwo(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)

但是,上面这段代码并没有修改超类构造器的调用顺序,它还是和以前一样,先调用 TimesTwo.__init__,然后才调用 PlusFive.__init__, 这就导致该类所产生的结果与其超类的定义顺序不相符。

bar = AnotherWay(5)
print('Second ordering still is', bar.value)   
>>>
Second ordering still is 15

还有一个问题发生在钻石形继承之中。如果子类继承自两个单独的超类,而那两个超类又继承自同一个公共基类,那么就构成了钻石形继承体系。这种继承会使钻石顶部的那个公共基类多次执行 __init__ 方法,从而产生意想不到的行为。例如,下面定义的这两个子类,都继承自 MyBaseClass。

class TimesFive(MyBaseClass):
    def __init__(self):
        self.value *= 2

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

我们可能认为输出的结果会是27,因为 (5*5)+2=27,但实际上却是7,因为我们在调用第二个超类的构造器,也就是 PlusTwo.__init__ 的时候,它会再度调用 MyBaseClass.__init__ ,从而导致self.value 重新 变成5。
python2.2 增加了内置的super函数,并且定义了方法解析顺序(method resolution order, MRO),以解决这一问题。MRO以标准的流程来安排超类之间的初始化顺序(例如,深度优先、从左至右),它也保证钻石顶部那个公共基类的 __init__方法只会支行一次。
下面重新创建钻石形的继承体系,但是这一次,我们用super初始化超类(范例代码以python 2 的风格来使用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语句中出现的顺序相同。

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

看到上面的运行结果之后,我们可能觉得程序的计算顺序和自己所想的刚好相——反。 应该先运行 -imesFiveCorrect.__init__ ,然后运行 PlusTwoCorrect.__init__ ,并得出 (5*5)+2=27 才对啊。但实际上却不是这样的。程序的运行顺序会与GoodWay类的MRO保持一致,这个MRO顺序可以通过名为mro的类方法来查询。

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,使value变成7,最后, TimesFiveCorrect.__init__ 会将value乘以5, 使变为35。
python3提供了一种不带参数的super调用方式,该方式的效果与__class__ 和self来调用super相同 。Python3 总是可以通过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

由于python3程序可以在方法中通过 __class__ 变量准确地引用当前类,所以上面这种写法能够正常动作,而python2则没有定义__class__ ,故而能采用这种写法。你可能想试着用self.__class__做参数来调用super,但实际上这么做不行,因为python2是用特殊方式来实现super的。

要点:

  1. python采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题。
  2. 总是应该使用内置的super函数来初始化父类。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值