本記事について
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
(種族)を継承して、Human
・Elf
クラスを作っています。
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__
はいじらずに、Human
とElf
を継承してみます。
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_power
はHuman
にはなく、逆にHuman
存在するluck
はElf
にないため、どうやってもエラーを吐いてしまいます。
解決するためには、上記コードで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
が引き継がれません。
更に、単一継承ならHuman
はsuper
を使って基底クラスRace
のイニシャライザを呼び出しているはずですが、今回は多重継承なのでMROのルートに準拠して、Elf
を呼び出します。しかし、Human
では属性suction_power
が存在しません。そのため、インスタンス時に必ずエラーを吐きます。
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
クラス内のsuper
でElf
クラスを呼び出す時、super().__init__(この中)のプロパティに
suction_powerが無く、
Elf`のプロパティと一致しないため、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
で補ってあげます。
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を試してみたい。