LoginSignup
4
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-24

本記事について

Djangoをオーバーライドして機能を変えたいけどやり方がわからない...。そもそも多重継のコードを読み解くことができないので、勉強しました。本記事では、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):のように1つのクラスを継承することを単一継承といいます。

継承の順番

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

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

MROが教えてくれる

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


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'>)と表示されました。見た通りメソッドの探索順を表示できました。

多重継承とは?

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

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メソッドを実行すると何が出力されるでしょうか?

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が教えてくれる

上記の多重継承のコードで試してみます。


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()でオーバーライドされるメソッドの順番が変わることは必ず覚えておきたいです


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(吸引力?)
 メソッド1  sword_skill bow_skill
 メソッド2  life_up  drain

クラスを定義

基底クラスRace(種族)を継承して、HumanElfクラスを作っています。

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}回復した!)')

定義したクラスで遊ぶ

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__はいじらずに、HumanElfを継承してみます。

イニシャライザは触らない
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を調べる


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_powerHumanにはなく、逆にHuman存在するluckElfにないため、どうやってもエラーを吐いてしまいます。

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

属性を調べる

HalfElfの属性何があるでしょうか?


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__を呼び出しているからです。

メソッドを調べる

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が引き継がれません。

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

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つだから

2つの親クラスで定義されているプロパティが異なること・多重継承のメソッド探索ルートがMROの順番に変わることが理由で、うまく動かないのだと思います。(難しい....!!)

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

super(HalfElf, self).__init__()

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

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ではsuperRaceクラスが呼び出されますが、ElfクラスのプロパティはRaceクラスのプロパティを満たしているため、エラーは置きません。その代わり、Humanクラスluckは一切引き継がれていないので、改めてself.luck = luckで補ってあげます。

うまく行った
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吸い取った!
'''

遊んでみて

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

4
4
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
4
4