こんにちは!フリーランスエンジニア・ライターの平山です。
皆さんの中にPythonの多重継承についてお悩みをお持ちの方はいませんか?
たとえば
「多重継承の継承順序がいまいちわからない・・・」
「多重継承はめんどくさいから使わないほうがいいって教わったんだけど・・・」
たしかに、多重継承は言語によってとても嫌われています。
ですが、Pythonの多重継承はさまざまな努力によってかなり使いやすく整備されているんです。
ですので、今回は
継承の基礎
から、
多重継承のやり方
までお伝えします。
また、多重継承で問題になる
菱形継承問題
についても解説し、Pythonがどのようにこの問題を解決しているのかも紹介します。
ぜひこの記事を読んで、多重継承を使いこなせるようになりましょう!
本記事を読む前に、Pythonがどんなプログラミング言語なのかをおさらいしておきたい人は次の記事を参考にしてください。
→ Pythonとは?特徴やできること、活用例をわかりやすく簡単に解説
なお、その他のPythonの記事についてはこちらにまとめています。
クラスの継承とは
まずはクラスの継承について復習しましょう。
クラスの継承とは、親クラスの特性を子クラスに引き継がせることを言います。
次の例のように親クラスで定義したメソッドは子クラスでも利用できます。
class Oya(): def oya_func(self): print("I am OYA") class Kodomo(Oya): def kodomo_func(self): print("I am Kodomo") k = Kodomo() k.oya_func() k.kodomo_func() 実行結果 I am OYA I am Kodomo 1 2 I am OYA I am Kodomo
ここまでわかりやすさ優先で親クラス、子クラスという表現をしてきました。
ですが、参考書などでは以下のように書かれることが多々あります。
用語として、ぜひ覚えておきましょう。
- 親クラス = スーパークラス
- 子クラス = サブクラス
クラスの基礎について不安がある場合は次の記事を読んでみてください。
スーパークラスのメソッドは次にのようにサブクラス内部で呼び出すことができます。
class Oya(): def __init__(self, name): self.name = name print("I am " + str(name)) class Kodomo(Oya): def __init__(self, name): super().__init__(name) self.age = "18" print("I am " + self.age + " years old") K = Kodomo("Taro") # 結果 I am Taro I am 18 years old
まず、スーパークラスのコンストラクタで名前を取得・表示しています。
サブクラスでは、super()を使ってその機能を継承しているわけですね。
なお、super()は本来引数に継承元のオブジェクトとselfを持ちます。
class Kodomo(Oya): def __init__(self, name): super(Oya, self).__init__(name)
Python2系ではこの書き方が標準でした。
3系に移行してからは、引数を省略した先程の書き方が標準になっています。
バージョンをまたぐようなプログラミングをする人は要注意ですね。
このsuper()の使い方が多重継承では大きな意味を持ってきます。
それでは多重継承のやり方に進みましょう。
多重継承のやり方
この章では多重継承の方法を学んでいきましょう。
多重継承は複数のスーパークラスからサブクラスを作る方法です。
PHPやJavaでは多重継承ができないため、Pythonで初めて見る方も多いかもしれません。
多重継承をつかったクラスの作成は簡単です。
次の例のように、クラスの引数に複数のスーパークラスを指定します。
これだけで、多重継承ができてしまいます。
class A(object): def __init__(self): pass class B(A): def __init__(self): super().__init__() pass class C(A): def __init__(self): super().__init__() pass # クラスDが多重継承 class D(B, C): def __init__(self): super().__init__() pass
これはどんな場面で役立つかというと
- 読み込みクラスと書き込みクラスがある時、読み書きクラスを作る
- 長方形クラスとひし形クラスがある時、正方形クラスを作る
などがあります。
上手に使うことで、既存のものからより手軽に、より便利なクラスが作れるのが多重継承の魅力ですね。
菱形継承問題に対応する
前章で多重継承のメリットについて説明してきました。
ここからは、多重継承のデメリットについてもお伝えします。
多重継承は言語によって採用されていないと説明しました。
これはなぜかと言うと、多重継承にはいろいろとややこしい点があるからです。
その中でも有名なものが、菱形継承問題と呼ばれるものです。
菱形継承問題とは
大親クラスAを継承して親Bと親Cを作る。
親Bと親Cを多重継承して子Dをつくる。
Aで定義されたメソッドをBとCでオーバーライドした。
BとCを継承したDではオーバーライドしていない。
このときメソッドはBとCどちらが使われるのか?という問題。
継承を図示すると下図のように菱形になるため、「菱形継承問題」と呼ばれています。
人間の目からすれば、例えば左の方の引数を優先すればいいだけじゃん、という気もします。
ですが、これをコンピュータに処理させようとすると様々な困難がともなうのです。
実際、Pythonも2.1以前は探索アルゴリズムが整備されていませんでした。
そのため、B→A→C→Aとあまり効率の良くない探査をしていました。
Aが2回参照されているのがマズイ点です。
現在ではC3線形化アルゴリズムという便利なものが整備されました。
その結果、より効率的に探査されるようになったのです。
論理的背景が気になる方は下記リンク先をご参照ください。
URL:https://docs.python.jp/3/tutorial/classes.html#multiple-inheritance
URL:https://www.python.org/download/releases/2.3/mro/
実際にPythonがどのような順番でクラスを実行しているのかを直接見る方法があります。
それが、次に紹介するmro()です。
mro()の使い方
mroはMethod Resolution Order(メソッド解決順序)の略称です。
どんな順番でメソッドが解決されていくか、つまりどのクラスを参照していくのかを見ることができる関数です。
実際に動いているところを見てみたほうがわかりやすいでしょう。
class A(object): def __init__(self): print('A') class B(A): def __init__(self): super().__init__() print ('B') class C(A): def __init__(self): super().__init__() print ('C') class D(B, C): def __init__(self): super().__init__() print ('D') print(D.mro()) # 実行結果 [<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>]
このようにクラス名.mro()と書くのがポイントです。
つい、インスタンス化のクセでクラス名().mro()とクラス名にカッコをつけてしまうとエラーとなります。
ご注意ください。
結果を見てみると、D→B→C→Aととても効率よく探索してますね。
最後にMixinについて少し触れていきます。
PythonでMixinを使う
前章で菱形継承問題とその対処について解説しました。
これ以外にも、多重継承にまつわる問題はいくつかあります。
たとえば次のようなケースです。
class Hoge(object): def __init__(self, x): self.x = x def name(self): print("I am " + str(self.x)) class Fuga(object): def __init__(self, x): self.x = x def favorite(self): print("I love " + str(self.x) + "!!!") class HogeFuga(Hoge, Fuga): def __init__(self, x): super().__init__(x) def print_x(self): print(self.x) HF = HogeFuga("Taro") HF.name() HF.favorite()
コードの意図としては、
- Hoge クラスで名前を取得
- Fuga クラスで好きなものを取得
- HogeFuga クラスで名前と好きなものを表示したい
というものでした。
ですが、実際はというと
I am Taro I love Taro!!!
と、どっちにも人名が入ってしまっています。
これは、Hoge クラス、Fuga クラスどちらのメソッドもインスタンス変数xを参照するために起きています。
この例では当たり前過ぎて、実行する前から結果が予測できてしまいます。
ですが、継承が多段階になり、複数の親から多重継承したようなクラスを作ることは実践でよくあります。
このとき、往々にしてこのような変数の重複が起きてしまうのです。
それを回避するための手段のひとつがMixinです。
Mixinとは、
- インスタンス変数を継承できるスーパークラスはひとつのみにする。
- それ以外のスーパークラスはメソッドだけを継承させる。
という方法です。
実際にコードで見てみましょう。
class Hoge(object): def __init__(self, x): self.x = x def name(self): print("I am " + str(self.x)) class Fuga(object): # インスタンス変数をやめて、メソッドに変数を直接使った def favorite(self, item): print("I love " + str(item) + "!!!") class HogeFuga(Hoge, Fuga): def __init__(self, x): super().__init__(x) def print_x(self): print(self.x) HF = HogeFuga("Taro") HF.name() HF.favorite("apple")
このようにすることで、先程の「インスタンス変数が重複する問題」を回避できました。
Mixinは継承されることで初めて効果を発揮するクラス、とも言えます。
Rubyではこのようなクラスをモジュールと呼びます。
Rubyは多重継承ができなくする代わりに、モジュールをMixinさせる仕組みを使って多重継承と同等の機能を提供しています。
言語による考え方の違いが大きくあらわれている部分ですね。
Pythonでは多重継承もMixinもどちらも使えるので、必要に応じて手段を使い分けましょう。
多重継承の順番とmixin の使い方・まとめ
いかがでしたか?
今回は次のことを紹介しました
- Pythonの継承
- 多重継承のやり方
- 菱形継承問題の対処
- PythonのMixin
Python以外の言語だと、多重継承は複雑さが増すため、嫌われやすい傾向にあります。
ですが、Pythonでは長年に渡り、継承順位の整備が進められてきたため、かなり使いやすくなっています。
ぜひ、多重継承を使いこなして、より便利なクラスを作り上げましょう。
そして、多重継承で引っかかることがあれば、またこの記事を読みなおしてください。