0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

pythonでやってしまってハマったこと①

Posted at

多重継承で、特定のコンストラクタが呼ばれない

pythonで多重継承したとき、継承元の基本クラスの中に、super().__init__()してないクラスが存在すると、インタプリタが MRO(メソッド解決順序)を追えなくなるので、多重継承の2つ目のコンストラクタが呼ばれないという現象が発生します。

たとえば、以下のようなケースでは、

test_ng.py
class A:
    def __init__(self):
        print('>A', end='')

class B(A):
    def __init__(self):
        print('>B', end='')
        super().__init__()

class C:
    def __init__(self):
        print('>C', end='')
        super().__init__()

class D(C):
    def __init__(self):
        print('>D', end='')
        super().__init__()

class E(B, D):
    def __init__(self):
        print('>E', end='')
        super().__init__()

x=E()
# >>> E>B>A # 続きの >D>C が出てこない...

上記の例で、x=E() を実行すると、E>B>A までしか表示されません。

本例の正解のパターンは、

test_ok.py
class A:
    def __init__(self):
        print('>A', end='')
        super().__init__() # <- これが必要。
        # 実は objectの__init__() が呼ばれるわけではなく、class D の__init__() が呼ばれる!

class B(A):
    def __init__(self):
        print('>B', end='')
        super().__init__()

class C:
    def __init__(self):
        print('>C', end='')
        super().__init__()

class D(C):
    def __init__(self):
        print('>D', end='')
        super().__init__()

class E(B, D):
    def __init__(self):
        print('>E', end='')
        super().__init__()

x=E()
# >>> E>B>A>D>C

これに気づくのに、数日の時間を浪費ました。

python インタプリタは、実行時には、super().__init__() の呼び出しによってMRO順序を元に親のインスタンスを辿り、オブジェクトを初期化して行くという動きをします。

super().__init__()super() が、MROを検索するという動きをするからです。
具体的な処理の流れは以下のようになります。

順序  クラス  クラスに保持されるMRO順序 クラスのsuper().__init__()
が呼び出すメソッド
1. E E>B>A>D>C>object クラスB__init__(self)
2. B B>A>D>C>object クラスA__init__(self)
3. A A>D>C>object クラスD__init__(self)
4. D D>C>object クラスC__init__(self)
5. C C>object 暗黙のobjectクラスの def __init__(self)

前例の test_ng.py では、上記3番の、super().__init__()の呼び出しが無かったために、
クラスD__init__(self) が呼ばれず、処理が滞っていたということです。

なお、MROの並びは、

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.C'>, <class 'object'>)

# または

>>> E.mro()
(<class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.C'>, <class 'object'>)

# または、
# 型Eが持つMRO順序のうち型Eからリストアップする

>>> type(E).mro(E)
[<class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.C'>, <class 'object'>]
>>>

で確認することができます。

ベースクラスのコンストラクタに引数があるようなケース

滅多にないとは思いますが、下記の例のような実装では、それぞれのコンストラクタの引数が異なるような場合、エラーになります。

test_case2.py
class A:
    def __init__(self, name1, name2):
        print(f'>A("{name1}", "{name2}")', end='')
        super().__init__()

class B:
    def __init__(self, name3):
        print(f'>B("{name3}")', end='')
        super().__init__()

class E(B, A):
    def __init__(self):
        print('>E', end='')
        super(E, self).__init__("name3")
        super(B, self).__init__("name1", "name2")

# >>> x=E()
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 4, in __init__
#   File "<stdin>", line 4, in __init__
# TypeError: __init__() missing 2 required positional arguments: 'name1' and 'name2'
# >E>B("name3")>>>

super().__init__() を呼び出す限り、MROの検索が行われ上位へ処理が進んでしまうため、引数の数の相違がおきてしまいエラーとなります。

このケースでは、MROの検索が先に進まない様に、super().__init__()を止めればエラーは出ません。しかし、本末転倒的なやり方であり適切とは言えません。デフォルトのパラメータを使用するなど、引数の取り方を工夫するなどして回避策を考えるべきと思います。

test_case3.py
class A:
    def __init__(self, name1, name2):
        print(f'>A("{name1}", "{name2}")', end='')
        #super().__init__()

class B:
    def __init__(self, name3):
        print(f'>B("{name3}")', end='')
        #super().__init__()

class E(B, A):
    def __init__(self):
        print('>E', end='')
        super(E, self).__init__("name3")
        super(B, self).__init__("name1", "name2")

# >>> x=E()
# >E>B("name3")>A("name1", "name2")>>>

super() メソッドの引数

super() メソッドには引数があります。
長年pythonに携わってきた方々には当たり前かもしれませんが、Pythonドキュメントには、具体的な説明がなく、代わって長々とMROとやらの素晴らしさについて語られていました。

super() メソッドの引数については、
C で書かれた Python のソースを見ると、およそ以下の説明の通りとなります。

  • 第1引数は、MRO上で検索を始める前の型(クラス)を指定します
    MRO順序が A>B>C>D>E のとき、B から検索をしたいのであれば、A を指定します。
  • 第2引数は、MROを保持するオブジェクトのインスタンスを指定します
     通常は、オブジェクト自身のスーパークラスにアクセスすることが目的なので self を指定するのが適切です。super() は、第2引数のオブジェクトの型を取得し、そこからMROリストを得て、第1引数に指定した型を検索し、その次の型から、親クラスを決定します。

本節のまとめ

MROの順序決定アルゴリズムは、C3線形アルゴリズム という様です。
そもそも、コード自体は後々動的に変わることはないのに、何故に、動的な動作による解決という手段を取り入れたのか? … ただ、コンパイラではないので、そこが動的でないと難しいのかもしれません。構文というものが存在するのだから、出来ないことはないと思う、と言うと、google の Gemini さんにも、さんざん諭されてしまいました。

皆様にお叱りを受けてしまうかもしれないけれども、30年前の javascipt や C++ が、驚くべき進化を遂げたように、pythonもいずれ進化して、MROなどというものを理解していなくても、多重継承が楽にできることに期待したいです。それまでに私が生きれいれば!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?