【Python】多重継承で継承するものは何？


# 本記事について
Djangoをオーバーライドして機能を変えたいけどやり方がわからない...。そもそも多重継のコードを読み解くことができないので、勉強しました。本記事では、Pythonの多重継承の仕組みや使い方などを整理して、最後に遊んでみたいと思います。

# 継承とは？
継承とは、クラスの機能を引き継ぐことです。

```python
# 基底クラス
class Coffee(object):
    def __init__(self, color, fragrance, price, taste, elapsed_time):
        self.color = color
        self.fragrance = fragrance
        self.price = price
        self.satisfaction = ((taste + fragrance) * price) - elapsed_time

    def drink(self):
        print(f'満足度が{self.satisfaction}点のホーフィーを飲む')


coffee = Coffee('brown', 10, 200, 10, 300)
coffee.drink() # <--- 満足度が3700点のホーフィーを飲む

# 継承する（派生クラス）
class Coffee2(Coffee): 
    pass # <- Coffeeと全く同じ機能を有する

class Nihonsyu(Coffee2):
    pass # <- Coffee2と全く同じ機能を有する
```

継承したクラスは、メソッドを追加したり、オーバーライドすることで、機能を追加できます。
`class Coffee2(Coffee): `のように１つのクラスを継承することを**単一継承**といいます。


# 継承の順番
上記のような単一継承の場合は、単純に継承元をたどっていけばわかります。
`Nihonsyu <- Coffee2 <- Coffee`

クラスの継承では、継承順にメソッドを探索して、そのメソッドを引き継ぎます。
ちなみに、複数の親クラスに同じメソッド名があるときは、一番最初に継承された親クラスのメソッドしか引き継げません。
ただし、親クラスのメソッドで`super()`が定義されていれば、その親クラス・そのまた親クラスの機能も全て引き継ぐことができます。
どちらにせよ、多重継承のメソッドをオーバーライドしたいなら、どんなメソッドが継承されるのか理解しなければなりません。そのためには、親クラスの継承の順番を知る必要がありそうです。

## MROが教えてくれる
MROとは、`Method Resolution Order: メソッド解決順序`といいます。これは、クラスを継承した時のメソッドが探索される順番です。
Pythonの基底クラス`object`には__mro__というメソッドがあり、これを使うとMROを知ることができるので、上記のコードで試してみます。

```python

print(Nihonsyu.__mro__)
# (<class '__main__.Nihonsyu'>, <class '__main__.Coffee2'>, <class '__main__.Coffee'>, <class 'object'>)
```
`(<class '__main__.Nihonsyu'>, <class '__main__.Coffee2'>, <class '__main__.Coffee'>, <class 'object'>)`と表示されました。見た通りメソッドの探索順を表示できました。


# 多重継承とは？
多重継承とは、クラスの引数に２つ以上のクラスを渡した場合の状態を言います。

```python
class A:
    def hello(self):
        print('Hello from A')

class B(A):
    pass

class C(A):
    def hello(self):
        print('Hello from C')

class D(B, C): # <- 多重継承
    pass
```
Dが多重継承されたクラスですが、Dのインスタンスでhelloメソッドを実行すると何が出力されるでしょうか？



```python
d = D()
d.hello()
# Hello from C
```
Hello from C が出力されました。つまり`クラスC`のメソッドを引き継いでいます。
`class D(B, C)`としているので、`クラスB`が優先され、クラスBがまるごと継承しているクラスAの`Hello from A`が出力されるかと思いましたが、なぜでしょうか。

# 多重継承の順番
多重継承の順番を決めるアルゴリズムには`深さ優先`や`幅優先`などあるようですが、Pythonでは`C線形型`というどちらでもないアルゴリズムを採用しているようです。これについては深い話になっていまうので省略しますが、多重継承の構造によってMROが変わり、冗長性の少ない探索をするようです。
とりあえず、`__mro__`を使ってMROがわかるので、調べてみましょう。


## MROが教えてくれる
上記の多重継承のコードで試してみます。

```python

print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
```
`(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)`となりました。

# 多重継承時の`__init__`の注意点
多重継承したときは、MROに従ってメソッド等を継承していきますが、同じ名前のメソッドは最初に継承したクラスのものしか引き継げないようです。同じ名前のメソッドが存在しないならよいですが、`__init__`はほぼ必ず存在するので、super()で引き継ぎたいクラスの__init__をしっかり呼び出して、適切に初期化処理をする必要があるようです。

又、以下のコード`class C`での`__init__`は、一見`objectクラス`の`__init__`をオーバーライドしているように見えますが、多重継承時は`MRO`の順番に沿ってメソッドを連鎖的にオーバーライドするようなので、実際は`C`の次の`B`の`__init__`をオーバーライドしているようです。

**多重継承時において、`super()`でオーバーライドされるメソッドの順番が変わることは必ず覚えておきたいです**

```python

class B:
    def __init__(self):
        self.b_value = 'B'
        print('class B init')
        super().__init__()

class C:
    def __init__(self):
        self.c_value = 'C'
        print('class C init')
        super().__init__()

class D(C, B):
    def __init__(self):
        print('class D init')
        super().__init__()
        
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class 'object'>)

d = D()
d

#↑ class D init
#  class C init
#  class B init
```

# 多重継承で遊んでみた
ここからちょっと長いです。自己満足です。
#### 設定
*〜ここは人間とエルフが存在する世界。それぞれが特有の属性・メソッドを持っている〜*

**全種族共通**
属性： `name`, `life`, `age`, `mana`, `stamina`
メソッド： `attack`, `magic`, `defence`, `down` 


**種族特有**

|   |　人間 　|    エルフ    |
|:--|-------:|:----------:|
|　 属性　　|luck(運)　|suction_power(吸引力？)|
| 　メソッド１　|sword_skill|bow_skill|
| 　メソッド２　|life_up　|drain|


### クラスを定義
基底クラス`Race`(種族)を継承して、`Human`・`Elf`クラスを作っています。



```python
import random

class Race(object):
    def __init__(self, name, age, life, mana, stamina):
        self.name = name
        self.age = age
        self.life = life
        self.mana = mana
        self.stamina = stamina
        
    def attack(self):
        self.stamina -= 10
        print(f'{self.name}はモンスターを殴った！')
        
    def magic(self):
       self.mana -= 10
       print(f'{self.name}モンスターを凍らせた！')
        
    def defence(self):
        self.life -= 1
        print(f'{self.name}は攻撃を防いだ！')
        
    def down(self):
        self.life -= 10
        print(f'{self.name}は攻撃を食らった！')
    
    
class Human(Race):
    def __init__(self, name, age, life, mana, stamina, luck):
        super(Human, self).__init__(name, age, life, mana, stamina)
        self.luck = luck
    
    def sword_skill(self):
        self.stamina -= 20
        self.life += 10
        print(f'{self.name}は素早き剣裁きでモンスターを屠った！')
        print(f'{self.name}はモンスターからライフを奪い取った！')
        
    # ランダム値でライフが回復する
    def life_up(self):
        self.life_incremental = self.life + random.randint(1, 5) * self.luck
        self.life = self.life + self.life_incremental
        print(f'神が味方した！（{self.name}のライフが{self.life_incremental}回復した！）')
        
    
class Elf(Race):
    def __init__(self, name, age, life, mana, stamina, suction_power):
        super(Elf, self).__init__(name, age, life, mana, stamina)
        self.suction_power = suction_power
    
    def bow_skill(self):
        self.stamina -= 20
        self.mana += 10
        print(f'{self.name}は遠くからモンスターを屠った！')
        print(f'{self.name}はモンスターからマナを吸い取った！')
        
    def drain(self):
        self.mana_incremental = self.mana + random.randint(1, 5) * self.suction_power
        self.mana = self.mana + self.mana_incremental
        print(f'神が味方した！{self.name}はマナが{self.mana_incremental}回復した！）')
```

### 定義したクラスで遊ぶ

```python
kirito = Human('キリト', 18, 300, 200, 200, 10)
kirito.attack()
kirito.magic()
kirito.defence()
kirito.down()
kirito.sword_skill()
kirito.life_up()

print('---' * 10)

shinon = Elf('シノン', 18, 200, 300, 200, 15)
shinon.attack()
shinon.magic()
shinon.defence()
shinon.down()
shinon.bow_skill()
shinon.drain()
```

```:結果
キリトはモンスターを殴った！
キリトモンスターを凍らせた！
キリトは攻撃を防いだ！
キリトは攻撃を食らった！
キリトは素早き剣裁きでモンスターを屠った！
キリトはモンスターからライフを奪い取った！
神が味方した！（キリトのライフが319回復した！）
------------------------------
シノンはモンスターを殴った！
シノンモンスターを凍らせた！
シノン攻撃を防いだ！
シノン攻撃を食らった！
シノンは遠くからモンスターを屠った！
シノンはモンスターからマナを吸い取った！
神が味方した！シノンはマナが375回復した！）
```

### ハーフエルフの誕生
クラス`Human`とクラス`Elf`を多重継承したクラス`HalfElf`を作ります。
**やりたいことは、共有属性・メソッドはもちろんのこと、種族特有の属性・メソッドをも全部併せ持った種族:ハーフエルフを作ることです**

まずは`__init__`はいじらずに、`Human`と`Elf`を継承してみます。

```python:イニシャライザは触らない
class HalfElf(Human, Elf):
    def super_drain(self):
        self.super_drain = (self.luck + self.suction_power) * random.randint(1, 5) 
        print(f'神が味方した！{self.name}はモンスターからマナを{self.super_drain}吸い取った！')
```

#### MROを調べる

```python

print(HalfElf.__mro__)
# (<class '__main__.HalfElf'>, <class '__main__.Human'>, <class '__main__.Elf'>, <class '__main__.Race'>, <class 'object'>)
```

`Human`を最初に探索しているようです。
多重継承では、`super(自クラス, self)`で呼び出されるクラスがMROの順番に変わるため、`Human`クラスの`super().__init__`は、`Elf`クラスの`__init__`のパラメータに自身の値を渡そうとします。しかし、`Elf`に存在する`suction_power`は`Human`にはなく、逆に`Human`存在する`luck`は`Elf`にないため、どうやってもエラーを吐いてしまいます。

解決するためには、上記コードで`super()`を使い、かつ`super()`の引数に適切なクラス名を渡してあげなければいけません。
それについては後ほど。


#### 属性を調べる
`HalfElf`の属性何があるでしょうか？

```python

asuna = HalfElf()
# TypeError: __init__() missing 6 required positional arguments: 'name', 'age', 'life', 'mana', 'stamina', and 'luck'
```
属性は、`name`, `age`, `life`, `mana`, `stamina`, `luck`を有しています。`suction_power`を有していません。
Humanの`__init__`を呼び出しているからです。


#### メソッドを調べる

```python
print(dir(HalfElf))
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'attack', 'bow_skill', 'defence', 'down', 'drain', 'life_up', 'magic', 'super_drain', 'sword_skill']
```
`['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'attack', 'bow_skill', 'defence', 'down', 'drain', 'life_up', 'magic', 'super_drain', 'sword_skill']
`となりました。

全て引き継がれています。

#### なぜ`suction_power`が引き継がれないのか？

イニシャライザについて記述していないので、MROで一番最初に探索するHumanクラスのイニシャライザと全く同じになるため、`suction_power`が引き継がれません。

更に、単一継承なら`Human`は`super`を使って基底クラス`Race`のイニシャライザを呼び出しているはずですが、今回は多重継承なのでMROのルートに準拠して、`Elf`を呼び出します。しかし、`Human`では属性`suction_power`が存在しません。そのため、インスタンス時に必ずエラーを吐きます。

```python:error
class HalfElf(Human, Elf):
    def super_drain(self):
        self.super_drain = (self.luck + self.suction_power) * random.randint(5, 10) 
        print(f'神が味方した！{self.name}はモンスターからマナを{self.super_drain}吸い取った！')
             
        
        
asuna = HalfElf('アスナ', 18, 200, 300, 200, 10, suction_power=10)
# TypeError: __init__() got an unexpected keyword argument 'suction_power'
# Humanのプロパティにsuction_powerが無いため

asuna = HalfElf('アスナ', 18, 200, 300, 200, 10)
# TypeError: __init__() missing 1 required positional argument: 'suction_power'
# 呼び出しているElfクラスのプロパティあるsuction_powerに値が渡されていないため

suna = HalfElf('アスナ', 18, 200, 300, 200, 10, 10)
# TypeError: __init__() takes 7 positional arguments but 8 were given
# Humanクラスの引数は全部で7つだから
```
２つの親クラスで定義されているプロパティが異なること・多重継承のメソッド探索ルートがMROの順番に変わることが理由で、うまく動かないのだと思います。（難しい....!!）

これを解決するために、以下のことを行いました。

### `super(HalfElf, self).__init__()`
自クラスの次クラス`Human`のイニシャライザを呼び出しました。
この場合、Humanクラスのイニシャライザが呼び出されます。しかし`Human`クラス内の`super`で`Elf`クラスを呼び出す時、`super().__init__(この中)のプロパティに`suction_power`が無く、`Elf`のプロパティと一致しないため、Errorがでます。
（多分...明確に理解できていません...）

```python:error
class HalfElf(Human, Elf):
    def __init__(self, name, age, life, mana, stamina, luck, suction_power):
        super(HalfElf, self).__init__(name, age, life, mana, stamina, luck)
        self.suction_power = suction_power
        
    def super_drain(self):
        self.super_drain = (self.luck + self.suction_power) * random.randint(5, 10) 
        print(f'神が味方した！{self.name}はモンスターからマナを{self.super_drain}吸い取った！')

asuna = HalfElf('アスナ', 18, 200, 300, 200, 10, 10)

'''
  File "Main.py", line 85, in <module>
    asuna = HalfElf('アスナ', 18, 200, 300, 200, 10, 10)
  File "Main.py", line 75, in __init__
    super(HalfElf, self).__init__(name, age, life, mana, stamina, luck)
  File "Main.py", line 30, in __init__
    super(Human, self).__init__(name, age, life, mana, stamina)
TypeError: __init__() missing 1 required positional argument: 'suction_power'
'''
```

### `super(Human, self).__init__()`
Humanクラスの次クラス`Elf`のイニシャライザを呼び出しました。
Elfでは`super`で`Race`クラスが呼び出されますが、`Elf`クラスのプロパティは`Race`クラスのプロパティを満たしているため、エラーは置きません。その代わり、`Human`クラス`luck`は一切引き継がれていないので、改めて`self.luck = luck`で補ってあげます。

```python:うまく行った
class HalfElf(Human, Elf):
    def __init__(self, name, age, life, mana, stamina, suction_power, luck):
        super(Human, self).__init__(name, age, life, mana, stamina, suction_power)
        self.luck = luck
        
    def super_drain(self):
        self.super_drain = (self.luck + self.suction_power) * random.randint(5, 10) 
        print(f'神が味方した！{self.name}はモンスターからマナを{self.super_drain}吸い取った！')

asuna = HalfElf('アスナ', 18, 200, 300, 200, 10, 10)
asuna.attack()
asuna.magic()
asuna.defence()
asuna.down()
asuna.sword_skill()
asuna.life_up()
asuna.bow_skill()
asuna.drain()
asuna.super_drain()

'''
アスナはモンスターを殴った！
アスナモンスターを凍らせた！
アスナ攻撃を防いだ！
アスナ攻撃を食らった！
アスナは素早き剣裁きでモンスターを屠った！
アスナはモンスターからライフを奪い取った！
神が味方した！（アスナのライフが229回復した！）
遠くからモンスターを屠った！
モンスターからマナを吸い取った！
神が味方した！アスナはマナが330回復した！）
神が味方した！アスナはモンスターからマナを200吸い取った！
'''

```

### 遊んでみて

`Human`と`Elf`を継承すればかんたんに`HalfElf`ができると思いましたが、全くそんなことはなく、とても難しかったです。
多重継承を作るために基底クラスのコードを書き換えてしまうと他のコードに影響がでるので、クラスの書き方についてもっと勉強したいです。次はMixinを試してみたい。