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

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

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

たとえば、以下のようなケースでは、
```python: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** までしか表示されません。

本例の正解のパターンは、
```python: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__()``<br/>が呼び出すメソッド|
|:---:|:-----:|--------------------:|----------------------------------------|
|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の並びは、
```python
>>> 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'>]
>>>
```
で確認することができます。

## ベースクラスのコンストラクタに引数があるようなケース
滅多にないとは思いますが、下記の例のような実装では、それぞれのコンストラクタの引数が異なるような場合、エラーになります。
```python: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__()``を止めればエラーは出ません。しかし、本末転倒的なやり方であり適切とは言えません。デフォルトのパラメータを使用するなど、引数の取り方を工夫するなどして回避策を考えるべきと思います。

```python: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 のソースを見ると、およそ以下の説明の通りとなります。
* 第１引数は、MRO上で検索を始める前の型（クラス）を指定します
MRO順序が **A>B>C>D>E** のとき、**B** から検索をしたいのであれば、**A** を指定します。
* 第２引数は、MROを保持するオブジェクトのインスタンスを指定します
　通常は、オブジェクト自身のスーパークラスにアクセスすることが目的なので **self** を指定するのが適切です。**super()** は、第2引数のオブジェクトの型を取得し、そこからMROリストを得て、第1引数に指定した型を検索し、その次の型から、親クラスを決定します。
 
## 本節のまとめ
MROの順序決定アルゴリズムは、**C3線形アルゴリズム** という様です。
そもそも、コード自体は後々動的に変わることはないのに、何故に、動的な動作による解決という手段を取り入れたのか？　… ただ、コンパイラではないので、そこが動的でないと難しいのかもしれません。構文というものが存在するのだから、出来ないことはないと思う、と言うと、google の Gemini さんにも、さんざん諭されてしまいました。

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