2012/04/23

ダイアモンド継承とsuper()

super()の役割

これは python 2.7 のお話。
python 3.x では異なっているかもしれない。

動的な実行環境下での複数の継承の共同をサポートすることです。 この用途は Python 特有で、静的にコンパイルされる言語や、単一の継承しかサポートしない言語では見られないものです。 これは複数の基底クラスが同じメソッドを実装する “diamond diagram” を実装できるようにします。 良い設計のために、このメソッドがすべての場合に同じ形式で呼び出せるべきです。 (呼び出しの順序が実行時に決定されることや、順序がクラスの階層の変更に対応することや、その順序には実行時まで未知の兄弟クラスが含まれえることが理由です) super()より
この文章の意味は、
super()は「適切」かつ「まんべんなく」上位クラスのメソッドを呼ぶための仕組みを一定の記述で提供する。
つまり、継承元の仕事をキッチリこなす。
ってことだ。

super()の正しい使い方

class A(object):
    def talk(self):
        print 'This is A.'

class B(A):
    def talk(self):
        super(B, self).talk()
        print 'This is B.'

class C(A):
    def talk(self):
        super(C, self).talk()
        print 'This is C.'

class D(B, C):
    def talk(self):
        super(D, self).talk()

def main():
    d = D()
    d.talk()

if __name__ == '__main__':
    main()
結果は
This is A.
This is C.
This is B.
確かにまんべんなく呼び出している。
何気にスゴい。

間違った使い方

class A(object):
    def talk(self):
        print 'This is A.'

class B(A):
    def talk(self):
        print 'This is B.'

class C(A):
    def talk(self):
        print 'This is C.'

class D(B, C):
    def talk(self):
        super(D, self).talk()

def main():
    d = D()
    d.talk()

if __name__ == '__main__':
    main()
結果は
This is B.
継承元のメソッドに super() が書かれてないと、適切に継承元を辿れない。
super(D, self)は継承元を辿ろうと、B→C→A の順に見ていく。
ところが、B.talk()には super() の呼び出しがないので、ここで継承元を辿るキッカケを失ってしまい、このような結果になるみたい。

ちなみに B→C→A の順序はclass D(B, C)と書いてあるから。
class D(C, B):
    def talk(self):
        super(D, self).talk()
とすると C→B→A の順で辿ろうとするので、
This is C.
となる。

D.talk()を宣言しないと

class A(object):
    def talk(self):
        print 'This is A.'

class B(A):
    def talk(self):
        super(B, self).talk();
        print 'This is B.'

class C(A):
    def talk(self):
        super(C, self).talk();
        print 'This is C.'

class D(B, C):
    pass

def main():
    d = D()
    d.talk()

if __name__ == '__main__':
    main()
結果は
This is A.
This is C.
This is B.
class D で、更に機能を足す必要がないなら、ダイヤモンド継承だからって処理を明示する必要がないのね。

特定のクラスの処理を実行するには

多重継承のとき、継承元をまんべんなくではなく、特定のクラスの処理だけを実行させたい場合もある。
例えば、ラジオとCDプレーヤのクラスからコンポを作ったときの再生ボタンの動作。
class Base(object):
    def play(self):
        print 'player initialize.'

class Radio(Base):
    def play(self):
        super(Radio, self).play()
        print 'radio played.'

class CDPlayer(Base):
    def play(self):
        super(CDPlayer, self).play()
        print 'CD played.'

class Compo(Radio, CDPlayer):
    pass

def main():
    compo = Compo()
    compo.play()
では、両方の音がなってしまう。
player initialize.
CD played.
radio played.
そんな場合は、こう書けばよさげ。
class Base(object):
    def play(self):
        print 'player initialize.'

class Radio(Base):
    def play(self):
        super(Radio, self).play()
        print 'radio played.'

class CDPlayer(Base):
    def play(self):
        super(CDPlayer, self).play()
        print 'CD played.'

class Compo(Radio, CDPlayer):
    def __init__(self):
        self._selector = 'radio'

    def selector(self, name):
        self._selector = name

    def play(self):
        if self._selector == 'radio':
            Radio.play(self)
        else:
            CDPlayer.play(self)

def main():
    compo = Compo()

    print '\nswitch to CDPlayer'

    compo.selector('cd')
    compo.play() # CDが鳴る予定

    print '\nswitch to radio'

    compo.selector('radio')
    compo.play() # ラジオが鳴る予定
結果。
switch to CDPlayer
player initialize.
CD played.

switch to radio
player initialize.
CD played.
radio played.
ラジオに切り替えたら、両方がなってしまう!?

ここら辺が super() の面倒なところみたい。
一応の解決策はあるけど、これってどうなんだろ。
class Base(object):
    def play(self):
        print 'player initialize.'

class Radio(Base):
    def play(self):
        Base.play(self)
        print 'radio played.'

class CDPlayer(Base):
    def play(self):
        Base.play(self)
        print 'CD played.'

class Compo(Radio, CDPlayer):
    def __init__(self):
        self._selector = 'radio'

    def selector(self, name):
        self._selector = name

    def play(self):
        if self._selector == 'radio':
            Radio.play(self)
        else:
            CDPlayer.play(self)

def main():
    compo = Compo()

    print '\nswitch to CDPlayer'

    compo.selector('cd')
    compo.play() # CDが鳴る

    print '\nswitch to radio'

    compo.selector('radio')
    compo.play() # CDが鳴る
結果。
switch to CDPlayer
player initialize.
CD played.

switch to radio
player initialize.
radio played.

0 件のコメント:

コメントを投稿