Pythonの継承・多重継承・super()を理解する

Pocket

はじめに

Django を使っていると mixin が頻繁に用いられる。今まで見て見ぬふりをしてきたが、何が起きているかしっかり理解するためには Python の継承や多重継承の仕様を理解する必要がある。とのことで、いろいろ実験してみたので結果を書いていきます。

継承

  • 親クラスの内容を小クラスに引き継がせること
  • 親クラス、小クラスはスーパークラス、サブクラス とも言う
  • class 小クラス(親クラス) と宣言すると継承が実現できる

以下の例では小クラスで親クラスの __init__() が呼ばれています。

class Parent: # ※1
    def __init__(self):
        print('Parent init')

class Child(Parent):
    pass

child = Child()
> Parent init

※1 一行目の class Parent:class Parent(): と書いても同じで、さらに class Parent(object): と同義です。継承元を明示しない場合、object クラスが継承元になり、省略可能となります。
小クラスで親クラスの持つものと同じ名前の関数を定義すると、小クラスの関数で親クラスの関数を上書きます。これがオーバーライド。

class Parent:
    def __init__(self):
        print('Parent init')

class Child(Parent):
    def __init__(self):
        print('Child init')

child = Child()
> Child init

super() を用いると親クラスの関数を呼び出すことができます。上の例では Parent の __init__() が上書れましたが、親クラス小クラス両方の __init__() を用いたいような場合は下記のように書きます。

class Parent:
    def __init__(self):
        print('Parent init')

class Child(Parent):
    def __init__(self):
        super().__init__() # ※2
        print('Child init')

child = Child()
> Parent init
> Child init

※2 super().__init__()super(Child, self).__init__() と同義です。Python2 まではこのように書く必要がありました。
これは Child クラスの親、つまり Parent クラスのコンストラクタ(init)を呼び出す、ということを示しています。
ここまでが普通の継承のお話です。(変数の継承とかもどっかで追記したい)

多重継承

  • 複数の親クラスを引き継ぐこと
  • class 小クラス(親クラス, 親クラス) という風に宣言していく

複数の親クラスを継承し、それらの親クラスが同じ名前の関数を持っていた場合どのような動きをするでしょう。試してみます。

class ParentA:
    def __init__(self):
        print('ParentA init')

class ParentB:
    def __init__(self):
        print('ParentB init')

class Child(ParentA, ParentB):
    pass

child = Child()
> ParentA init

Child が ParentA と ParentB を継承していて、ParentA のコンストラクタが呼ばれています。
多重継承していて同じ関数を持っている場合場合、最も左のクラスのものが採用されます。(下記の感じです)
class Child(ParentA, ParentB):
ここで、親クラスの関数を呼び出す super() を思い出してみます。

class ParentA:
    def __init__(self):
        print('ParentA init')

class ParentB:
    def __init__(self):
        print('ParentB init')

class Child(ParentA, ParentB):
    def __init__(self):
        super().__init__()

child = Child()
> ParentA init

super().__init__()super(Child, self).__init__() と同義でした。試しに super() の第一引数を Child から ParentA に変えてみます。

class ParentA:
    def __init__(self):
        print('ParentA init')

class ParentB:
    def __init__(self):
        print('ParentB init')

class Child(ParentA, ParentB):
    def __init__(self):
        super(ParentA, self).__init__()

child = Child()
> ParentB init

先程は ParentA のコンストラクタが呼ばれていましたが、今回は ParentB のコンストラクタが呼ばれています。先程、継承している親クラスの一番左のクラスの関数が参照されると書きましたが、イメージ的には super() で指定した(第一引数で渡した)クラスの一つ右のクラスの関数が参照される、という方がしっくりきます。

さらに試しに super() の第一引数を ParentA から ParentB に変えてみます。

class ParentA:
    def __init__(self):
        print('ParentA init')

class ParentB:
    def __init__(self):
        print('ParentB init')

class Child(ParentA, ParentB):
    def __init__(self):
        super(ParentB, self).__init__()

child = Child()

すると今度は ParentA のコンストラクタも ParentB のコンストラクタも呼ばれていません。実はこれ、ParentB が継承している object クラスのコンストラクタが呼ばれているんです。
それを確かめるために、 super(ParentB, self).__init__() の部分を super(ParentB, self).__init__('hoge') と変えて object クラスのコンストラクタに値を渡そうとしてみます。
すると下記のエラーが表示されます。

  File "test.py", line 13, in 
    child = Child()
  File "test.py", line 11, in __init__
    super(ParentB, self).__init__('hoge')
TypeError: object.__init__() takes no parameters

このように、 super(ParentB, self).__init__()object.__init__() を呼び出そうとしていることがわかります。

一体これが何に使えるのか

もともとDjango: formset を form に埋め込む という記事で、何も継承しない mixin クラスのコンストラクタが mixin 自身を super() で呼び出している記述があり、理解不能で意味を調べることから始まりました。
結論としては、「super()をうまく利用することで、コンストラクタを持たない子クラスでも複数の親クラスのコンストラクタを利用する」ことができます。

(もはやここからは人様に読んで頂くことを想定していませんが噛み砕きます。)
先程の多重継承の例で Child クラスが ParentA と ParentB 両方のコンストラクタを呼び出したい場合は、下記のように定義できます。

class ParentA:
    def __init__(self):
        print('ParentA init')

class ParentB:
    def __init__(self):
        print('ParentB init')

class Child(ParentA, ParentB):
    def __init__(self):
        super(Child, self).__init__()
        super(ParentA, self).__init__()

child = Child()
> ParentA init
> ParentB init

続いて、私が上記の記事で見たコードを簡易的に再現したものが下記です。

class ParentA:
    def __init__(self):
        super(ParentA, self).__init__() ※3
        print('ParentA init')

class ParentB:
    def __init__(self):
        print('ParentB init')
    
class Child(ParentA, ParentB):
    pass

child = Child()

これをパッと見ると、※3 の部分で object クラスのコンストラクタを呼び出して何をしているんだ??と思いましたが、これはまず(省略されていますが)Child クラス内で ParentA のコンストラクタ(init)が呼ばれ、その中で※3 が処理されるため、ParentA の一つ右の ParentB のコンストラクタが呼ばれるという構造になっています。
そのため、結果も下記のようになります。

> ParentA init
> ParentB init

Python は文法が明瞭でそんなに難しくないんだと思いますが、このあたりは比較的複雑で、いざいろいろ動かしてみるととても勉強になりました。Django などのフレームワークはこういった技術をそこかしこで使っているので、より深く理解するためには今回の理解が役立ちそうです。

Pocket

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です