Pythonの多重継承とMixinクラス
著者:Leonardo Giordani - 27/03/2020 Updated on Apr 8, 2020
はじめに
最近、このブログに書いたDjangoのクラスベースのビューに関する3つの古い記事をDjango 3.0にアップデートしました(こちら からご覧になれます)。そして、コードベースがコードの再利用を高めるためにmixinクラスを使っていることに改めて気づきました。また、Pythonではmixinがあまり普及していないことにも気付いたので、その間にOOP理論の知識をブラッシュアップしながら、mixinを探ってみることにしました。 この記事の内容を十分に理解するためには、OOPアプローチの2つの柱であるデリゲーション(特に継承によってどのように実装されるか)とポリモーフィズムを把握しておく必要があります。次の記事には、Pythonがこれらの概念をどのように実装するかを理解するために必要なすべてが含まれています。
デリゲーション:
ポリモーフィズム
多重継承: 素晴らしいものであると同時に、恐ろしいもの
一般的な概念
mixin を議論するためには、OOPの世界で最も議論されているテーマの一つである多重継承から始める必要があります。これは単純継承の概念を自然に拡張したもので、クラスが自動的にメソッドや属性の解決を別のクラス(親クラス)に委譲するというものです。
この後の議論に重要なのでもう一度言いますが、継承とは自動的な委譲の仕組みに過ぎません。
委譲は、コードの重複を減らすためにOOPで導入されました。あるオブジェクトが特定の機能を必要とするとき、それを別のクラスに(明示的にも暗黙的にも)委譲するだけなので、コードは一度だけ書かれることになります。
コード管理サイトの例を考えてみましょう。明らかに完全なフィクションであり、既存の製品に触発されたものではありません。以下のような階層構造を作ったとします。
code:ascii
アサイン可能なレビュー可能なアイテム
(assign_to_user, ask_review_to_user)
^
|
|
|
プルリクエスト
これにより、その要素で必要とされる特定のコードのみをプルリクエストに載せることができます。これは、ライブラリがコードに対して行っていることを、生きているオブジェクトに対して行っているのですから、素晴らしい成果です。メソッドコールとデリゲーションはオブジェクト間のメッセージに過ぎないので、デリゲーション階層は単なるネットワークシステムに過ぎません。
残念なことに、構成よりも継承を使うことで、逆説的にコードの重複を増やすシステムになることが多い。主な問題は、オブジェクトが任意の数の他のクラスにデリゲートできるコンポジションとは対照的に、継承は1つの他のクラス(親クラス)にのみ直接デリゲートできるという事実にあります。継承のこの制限は、あるクラスが他のクラスの機能を必要とするためにそのクラスを継承しても、そのクラスが必要としない、あるいは持つべきではない機能を受け取ってしまうことを意味します。
コード管理ポータルの例を続けて、課題を考えてみましょう。課題とは、システムに保存したいが、ユーザがレビューできないアイテムのことです。次のような階層を作ると
code: ascii
アサイン可能なレビュー可能なアイテム
(assign_to_user, ask_review_to_user)
^
|
|
+--------+--------+
| |
| |
issue プルリクエスト
(レビューできません)
これにより、その要素で必要とされる特定のコードのみをプルリクエストに載せることができます。これは、ライブラリがコードに対して行っていることを、生きているオブジェクトに対して行っているのですから、素晴らしい成果です。メソッドコールとデリゲーションはオブジェクト間のメッセージに過ぎないので、デリゲーション階層は単なるネットワークシステムに過ぎません。
残念なことに、構成よりも継承を使うことで、逆説的にコードの重複を増やすシステムになることが多い。主な問題は、オブジェクトが任意の数の他のクラスにデリゲートできるコンポジションとは対照的に、継承は1つの他のクラス(親クラス)にのみ直接デリゲートできるという事実にあります。継承のこの制限は、あるクラスが他のクラスの機能を必要とするためにそのクラスを継承しても、そのクラスが必要としない、あるいは持つべきではない機能を受け取ってしまうことを意味します。
コード管理ポータルの例を続けて、課題を考えてみましょう。課題とは、システムに保存したいが、ユーザがレビューできないアイテムのことです。次のような階層を作ると
code: ascii
アサイン可能なアイテム
(assign_to_user, ask_review_to_user)
^
|
|
+---------+--------------+
| |
| |
| レビュー可能な割り当て可能なアイテム
| (ユーザーにレビューを依頼)
| ^
| |
| |
issue プルリクエスト
しかし、この方法は、オブジェクトがあるクラスを継承する必要があるが、そのクラスの親ではない場合、すぐに実行可能ではなくなります。例えば、サイトに追加したいベストプラクティスのように、レビュー可能だが割り当てられない要素がある場合です。継承を使い続けたい場合、現時点での唯一の解決策は、アイテムのレビュー可能な性質を実装するコード(または割り当て可能な機能を実装するコード)を複製し、2つの異なるクラス階層を作成することです。
code: ascii
アサイン可能なアイテム +--------> レビュー可能なアイテム
(assign_to_user) | (ask_review_to_user)
^ | ^
| | |
| | |
| コード複製 |
| | |
+------+--------------+ | |
| | | |
| | V |
| レビュー可能でアサイン可能なアイテム |
| (ask_review_to_user) |
| ^ |
| | |
| | |
issue プルリクエスト ベストプラクティス
これは、新しいレビュー可能なアイテムが、割り当て可能なアイテムの属性を必要とするかもしれないことを考慮していないことに注意してください。残念ながら、このアプローチを変えられなければ、システムを安定した状態に保つために受け入れなければならない多くの妥協のうちの最初のものに過ぎないでしょう。
これは、生命体が複数の祖先(親、祖父母など)から特徴を受け継ぐことを模倣したものです。
上記のような状況は、プルリクエストが、割り当て機能を提供するクラスと、レビュー可能な性質を実装するクラスの両方を継承することで解決できます。
code: ascii
アサイン可能なアイテム レビュー可能なアイテム
(assign_to_user) (ask_review_to_user)
^ ^ ^
| | |
| | |
| | |
+------+--------------+ +-------------------+ |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
issue プルリクエスト ベストプラクティス
一般的に多重継承は、プログラマーがコードを重複させることなく継承を使い続け、クラス階層をよりシンプルでクリーンなものにするために導入されます。最終的には、ソフトウェア設計で行うことは、懸念事項を分離すること、つまり、機能を分離することであり、多重継承はそのために役立ちます。
これらはあくまでも例であり、具体的なケースに応じて有効な場合もあればそうでない場合もありますが、4つのクラスからなる非常にシンプルな階層であっても、問題があることは明らかです。これらの問題の多くは、委譲を継承によってのみ実装しようとしたことに起因しています。私は、OOPプロジェクトにおけるアーキテクチャ上のエラーの80%は、構成の代わりに継承を使用したことと、ゴッドオブジェクト(システムのあまりに多くの異なる部分に責任を持つクラス)を使用したことに起因していると断言します。OOPは、小さなオブジェクトがメッセージを介して相互作用するというアイデアから生まれたものであり、モノリシックなアーキテクチャで考慮すべきことは、ここでも有効であることを忘れてはなりません。
とはいえ、継承とコンポジションは2つの異なるタイプの委任(to beとto have)を実装しているので、どちらも価値があり、多重継承は親クラスが1つしかないことからくる単一プロバイダの制限を取り除く方法です。
議論の余地がある理由は?
先ほど述べたことを考えると、多重継承は祝福すべきことのように思えます。一つのオブジェクトが複数の親クラスを継承することができれば、複数のクラスに責任を分散させ、必要なものだけを使用することができ、コードの再利用を促進し、神のオブジェクトを避けることができます。
しかし、残念ながらそう簡単にはいきません。つまり、ゴッドオブジェクト(極端なモノリシックアーキテクチャ)から、ほとんど何もないオブジェクト(極端な分散アプローチ)になってしまう危険性があります。
多重継承には、もっと直接的な問題があります。自然界の遺伝と同じように、親は同じ「遺伝形質」を2つの異なる味で提供することができますが、結果として生まれる個体は1つしか持ちません。遺伝の話はさておき(これはプログラミングとは比べ物にならないほど複雑です)、OOPの話に戻ると、あるオブジェクトが同じ属性を持つ2つのオブジェクトを継承した場合に問題が発生します。
例えば、ChildクラスがParent1とParent2を継承していて、どちらも__init__メソッドを提供している場合、どちらを使うべきでしょうか?
code: python
class Parent1():
def __init__(self):
class Parent2():
def __init__(self):
class Child(Parent1, Parent2):
# このクラスはParent1とParent2の両方を継承していますが、どの__init__を使用していますか?
pass
さらに悪いことに、親は共通のメソッドに異なるシグネチャを持つことがあります。
code: python
class Parent1:
# 祖先を継承していますが、__init__を再定義している
def __init__(self, status):
class Parent2:
# 祖先を継承していますが、__init__を再定義している
def __init__(self, name):
class Child(Parent1, Parent2):
# このクラスはParent1とParent2の両方を継承していますが、どの__init__を使用していますか?
pass
この問題はさらに拡大して、Parent1 とParent2の上に共通の祖先を導入することができます。
code: python
class Ancestor:
# 共通の祖先は、自身の __init__ を定義している
def __init__(self):
class Parent1:
# 祖先を継承していますが、__init__を再定義している
def __init__(self, status):
class Parent2:
# 祖先を継承していますが、__init__を再定義している
def __init__(self, name):
class Child(Parent1, Parent2):
# このクラスはParent1とParent2の両方を継承していますが、どの__init__を使用し
ていますか?
pass
ご覧のように、複数の親を導入する際にはすでに問題があり、共通の祖先を持つことで新たな複雑さが加わることになります。祖先クラスは継承ツリーのどの位置にあっても構いませんが(祖父母、祖父母など)、重要なのはParent1とParent2の間で共有されていることです。これは、継承グラフがダイヤモンドの形をしていることから、いわゆるダイヤモンド問題と呼ばれています。
code: ascii
Ancestor
^ ^
/ \
/ \
Parent1 Parent2
^ ^
\ /
\ /
Child
つまり、片親の相続ではルールは簡単ですが、多重相続ではすぐに複雑な状況になり、一筋縄では解決できないのです。これでは、多重継承は実現できないのでは?
そんなことはありません。しかし、このように複雑になると、多重継承は簡単にはデザインに組み込めなくなり、微妙なバグを避けるために慎重に実装しなければなりません。継承は自動的に委譲される仕組みであることを忘れてはいけません。これにより、コードの中で何が起こっているのかが明らかになりません。これらの理由から、多重継承は怖くて複雑なものとして描かれ、少なくともPythonの世界では、上級のOOPコースでのみスペースが与えられています。しかし、私はすべてのPythonプログラマーが多重継承に慣れ親しみ、その活用方法を学ぶべきだと考えています。
多重継承:Pythonの方法
それでは、ダイヤモンド問題を解決する方法を見てみましょう。遺伝学とは異なり、私たちプログラマーはプロセスに不確実性やランダム性を許されません。そのため、多重継承のような曖昧さが生じる可能性がある場合には、どのような場合でも厳密に守られるルールを記述する必要があります。Pythonでは、このルールはMRO (Method Resolution Order) という名前で、Python 2.3で導入され、Michele SimionatoによるドキュメントThe Python 2.3 Method Resolution Orderで説明されています。 MROとその基礎となるC3線形化アルゴリズムについては多くのことを語る必要がありますが、この記事の範囲では、ダイヤモンド問題をどのように解決するかを見れば十分です。多重継承の場合、Pythonは通常の継承ルール(ローカルに属性が存在しない場合、祖先への自動委譲)に従いますが、継承ツリーを辿る順序には、クラスシグネチャで指定された全てのクラスが含まれるようになりました。上の例では、Pythonは以下の順序で属性を探します。Child, Parent1, Parent2, Ancestor.
つまり、標準的な継承の場合と同様に、特定の属性を実装しているリストの最初のクラスが、その解決のために選択されたプロバイダになることを意味します。例を挙げて説明します。
code: python
class Ancestor:
def rewind(self):
print("Ancestor: rewind")
class Parent1(Ancestor):
def open(self):
print("Parent1: open")
class Parent2(Ancestor):
def open(self):
print("Parent2: open")
def close(self):
print("Parent2: close")
def flush(self):
print("Parent2: flush")
class Child(Parent1, Parent2):
def flush(self):
print("Child: flush")
print(Child.__mro__)
c = Child()
c.rewind()
c.open()
c.close()
c.flush()
ご覧のように、任意のクラスの__mro__属性を読み取ることで、そのMROにアクセスすることができ、その値は予想通り(<class '__main__.Child'>, <class '__main__.Parent1'>, <class '__main__.Parent2'>, <class '__main__.Ancestor'>, <class 'object'>)となっています。
つまり、この場合、Childのインスタンスcはrewind()、open()、close()、flush()を提供します。c.rewind()が呼ばれると、MROリストの中で最初にそのメソッドを提供しているクラスであるAncestorのコードが実行されます。メソッドopen() は Parent1 が、close() は Parent2 が提供しています。メソッド c.flush() が呼ばれた場合、コードはChildクラス自身が提供し、Parent2が提供したものをオーバーライドして再定義します。
flush()メソッドで見たように、Pythonは複数の親を持つメソッドのオーバーライドに関しては、その挙動を変えません。その名前のメソッドの最初の実装が実行され、親の実装が自動的に呼び出されることはありません。標準的な継承の場合と同様に、メソッドのシグネチャが一致するようにクラスを設計するのは私たちの責任です。
どうなっているのか調べてみる
多重継承は内部的にどのように機能しているのでしょうか?PythonはどのようにしてMROリストを作成するのですか?
PythonのOOPアプローチは非常にシンプルです(最終的には元に戻る話なのですが、次の記事をご覧ください)
クラスはオブジェクトそのものなので、言語が機能を提供するために使用するデータ構造を含んでおり、委譲も同じことです。オブジェクトに対してメソッドを実行すると、Pythonは黙って__getattribute__()メソッド(objectクラスで提供される)を使用します。このメソッドは、インスタンスからクラスに到達するために__class__を使用し、親クラスを見つけるために__bases__を使用します。特に後者はタプルなので順序が決まっており、現在のクラスが継承している全てのクラスを含んでいます。
MROは__bases__のみを使用して作成されますが、基礎となるアルゴリズムはそれほど簡単なものではなく、結果として得られるクラスの線形化の単調性に関係しています。案外怖くないのですが、日焼けしながら読みたい内容ではないかもしれません。そんなときは、前述のMichele Simionato氏のドキュメントに、ビーチで寝転びながら読みたいクラス線形化の詳細がすべて書かれています。
継承とインターフェース
mixin にアプローチするためには、継承について詳しく説明する必要があり、特にメソッドシグネチャの役割について説明します。
Pythonでは、祖先のクラスが提供するメソッドをオーバーライドする際に、元の実装を呼び出すかどうか、またいつ呼び出すかを決定しなければなりません。これにより、プログラマはメソッドを拡張するだけなのか、完全に置き換える必要があるのかを自由に決めることができます。クラスが他のクラスを継承するときにPythonがすることは、実装されていないメソッドを自動的に委譲することだけだということを覚えておいてください。
クラスが他のクラスを継承するとき、理想的には親クラスのインターフェイスとの下位互換性を維持したオブジェクトを作成し、それらを多義的に使用できるようにします。つまり、クラスを継承してメソッドをオーバーライドしてシグネチャを変更することは、危険なことであり、少なくともポリモーフィズムの観点からは間違ったことをしていることになります。以下の例を見てください。
code: python
class GraphicalEntity:
def __init__(self, pos_x, pos_y, size_x, size_y):
self.pos_x = pos_x
self.pos_y = pos_y
self.size_x = size_x
self.size_y = size_y
def move(self, pos_x, pos_y):
self.pos_x = pos_x
self.pos_y = pos_y
def resize(self, size_x, size_y):
self.size_x = size_x
self.size_y = size_y
class Rectangle(GraphicalEntity):
pass
class Square(GraphicalEntity):
def __init__(self, pos_x, pos_y, size):
super().__init__(pos_x, pos_y, size, size)
def resize(self, size):
super().resize(size, size)
Squareでは、__init__とresizeの両方のシグネチャが変更されていることに注意してください。さて、これらのクラスをインスタンス化する際には、Squareでは__init__のシグネチャが異なることを念頭に置く必要があります。
code: python
r1 = Rectangle(100, 200, 15, 30)
r2 = Rectangle(150, 280, 23, 55)
q1 = Square(300, 400, 50)
私たちは通常、あるクラスの拡張バージョンが初期化時に異なるパラメータを受け付けることを受け入れますが、これは__init__でポリモーフィズムを持つことを期待していないからです。問題が生じるのは、他のメソッドに多相性を利用しようとしたときです。たとえば、リストに含まれるすべてのGraphicalEntityオブジェクトのサイズを変更するには
code: python
size_x = shape.size_x
size_y = shape.size_y
shape.resize(size_x*2, size_y*2)
r1、r2、q1はすべてGraphicalEntityを継承したオブジェクトなので、そのクラスが提供するインターフェイスを提供することが期待されますが、Squareがresizeのシグネチャを変更したため、これは失敗します。クラスのリストからforループでインスタンス化しても同じことが起こるでしょうが、先ほど言ったように、一般的には子クラスが__init__メソッドのシグネチャを変更すると考えられています。これは、たとえばプラグインベースのシステムでは、すべてのプラグインが同じように初期化されなければならないというような場合には当てはまりません。
これはOOPにおける典型的な問題です。私たち人間は、正方形を少し特殊な長方形として認識していますが、インターフェイスの観点からすると、この2つのクラスは異なるものであり、次元を扱う場合には同じ継承ツリーに入れるべきではありません。これは重要な検討事項です。RectangleとSquareはmoveメソッドではポリモーフィックですが、__init__ と resize ではポリモーフィックではありません。つまり、移動可能とサイズ変更可能という2つの性質をなんとか分けられないかということです。
さて、インターフェイスやポリモーフィズム、そしてその理由について議論すると、まったく別の記事が必要になるので、以下のセクションでは、この問題を無視して、オブジェクト・インターフェイスは任意であると考えることにします。そのため、親のインターフェースを壊すオブジェクトと、それを維持するオブジェクトの例が出てきます。覚えておいてほしいのは、メソッドのシグネチャを変更すると、必ずオブジェクトの(暗黙の)インターフェイスを変更することになり、結果としてポリモーフィズムが停止するということです。これが正しいのか間違っているのかは、また別の機会にお話します。
mixinクラス
MROは曖昧さを防ぐ良い解決策ですが、プログラマには賢明な継承ツリーを作成する責任があります。アルゴリズムは複雑な状況を解決するのに役立ちますが、だからといって、そもそもそれを作るべきではありません。では、複雑すぎるシステムを作らずに多重継承を活用するにはどうすればいいのでしょうか。また、先ほどの「移動可能でサイズ変更可能な形状」の例のように、オブジェクトの二重性(複数性)を管理する問題を解決するために多重継承を利用することは可能でしょうか?
mixin は、属性を提供する小さなクラスですが、標準的な継承ツリーには含まれておらず、本来の祖先としてではなく、現在のクラスへの「追加」として機能します。mixin の起源はLISPプログラミング言語にあり、特にCommon Lisp Object Systemの最初のバージョンともいえるFlavors拡張にあります。現代のOOP言語では、様々な方法で mixin を実装しています。例えば Scala には traits という機能があります。 traits は特定の階層を持つ独自の空間に存在し、適切なクラス継承を妨げません。
Pythonのmixinクラス
Pythonでは専用の言語機能でmixin をサポートしていないので、多重継承を使って実装しています。これは明らかに、mixin の主な前提条件の一つである継承ツリーへの直交性に違反しているため、プログラマーには大きな規律が求められます。
訳注 :直交性(Orthogonality)
関数やメソッドは他の関数やメソッドに影響があってはならない。そのため、凝集性があり、独立的なコードを作成する必要があります。この2つの条件(凝集、独立)を満たすとき直交性があるといい、コードを分離して簡単に保守することができます。もっと簡単にいうと、直交性とは、コードを修正した時に、変更しようとした箇所以外に影響がないこと。
直交性に違反するということは、何かしらの影響を与えてしまうということ。
Pythonでは、mixin と呼ばれるクラスは通常の継承ツリーの中に存在しますが、プログラマーが把握できないほど複雑な階層を作らないように小さく保たれています。特に、mixin は他の親クラスとobjectクラス以外の共通の祖先を持つべきではありません。
簡単な例を見てみましょう。
code: python
class GraphicalEntity:
def __init__(self, pos_x, pos_y, size_x, size_y):
self.pos_x = pos_x
self.pos_y = pos_y
self.size_x = size_x
self.size_y = size_y
class ResizableMixin:
def resize(self, size_x, size_y):
self.size_x = size_x
self.size_y = size_y
class ResizableGraphicalEntity(GraphicalEntity, ResizableMixin):
pass
rge = ResizableGraphicalEntity(5, 4, 200, 300)
rge.resize(1000, 2000)
ここでは、ResizableMixinクラスはGraphicalEntityからの継承ではなく、objectクラスから直接継承しているので、ResizableGraphicalEntityはresizeメソッドだけを取得しています。先に述べたように、これによりResizableGraphicalEntityの継承ツリーが単純化され、ダイヤモンド問題のリスクを減らすことができます。これにより、GraphicalEntityを他のクラスの親として使用する際に、必要のないメソッドを継承する必要がなくなります。MROアルゴリズムは、複数の祖先がいる場合に、常に明確な選択ができるようにしているだけです。
mixin は通常、あまり一般的ではありません。結局のところ、mixin はクラスに機能を追加するために設計されていますが、これらの新しい機能はしばしば、拡張されたクラスの既存の他の機能と相互作用します。この例では、resizeメソッドは、オブジェクトに存在しなければならない属性size_xとsize_yと相互作用します。もちろん、純粋なmixin の例もありますが、初期化が必要ないため、その範囲は限られています。
継承をハイジャックするためのmixin の使用
MRO のおかげで、Python プログラマーは多重継承を利用して、オブジェクトが親から継承するメソッドをオーバーライドすることができ、コードを重複させることなくクラスをカスタマイズすることができます。以下の例を見てみましょう。
code: python
class GraphicalEntity:
def __init__(self, pos_x, pos_y, size_x, size_y):
self.pos_x = pos_x
self.pos_y = pos_y
self.size_x = size_x
self.size_y = size_y
class Button(GraphicalEntity):
def __init__(self, pos_x, pos_y, size_x, size_y):
super().__init__(pos_x, pos_y, size_x, size_y)
self.status = False
def toggle(self):
self.status = not self.status
b = Button(10, 20, 200, 100)
ご覧のように、ButtonクラスはGraphicalEntityクラスを古典的な方法で継承しており、新しいstatus属性を追加する前にsuper()を使って親の__init__()メソッドを呼び出しています。さて、もしSquareButtonクラスを作ろうと思ったら、2つの選択肢があります。
まず、新しいクラスで__init__()をオーバーライドする方法です。
code: python
class GraphicalEntity:
def __init__(self, pos_x, pos_y, size_x, size_y):
self.pos_x = pos_x
self.pos_y = pos_y
self.size_x = size_x
self.size_y = size_y
class Button(GraphicalEntity):
def __init__(self, pos_x, pos_y, size_x, size_y):
super().__init__(pos_x, pos_y, size_x, size_y)
self.status = False
def toggle(self):
self.status = not self.status
class SquareButton(Button):
def __init__(self, pos_x, pos_y, size):
super().__init__(pos_x, pos_y, size, size)
b = SquareButton(10, 20, 200)
これは、要求された仕事を実行しますが、単一の次元を持つという特徴をButtonの性質と強く結び付けています。もし、円形のイメージを作成したい場合、イメージの性質が異なるため、SquareButtonを継承することはできません。
2つ目の方法は、単一の寸法を持つ機能をmixinクラスで分離し、それを新しいクラスの親として追加する方法です。
code: python
class GraphicalEntity:
def __init__(self, pos_x, pos_y, size_x, size_y):
self.pos_x = pos_x
self.pos_y = pos_y
self.size_x = size_x
self.size_y = size_y
class Button(GraphicalEntity):
def __init__(self, pos_x, pos_y, size_x, size_y):
super().__init__(pos_x, pos_y, size_x, size_y)
self.status = False
def toggle(self):
self.status = not self.status
class SingleDimensionMixin:
def __init__(self, pos_x, pos_y, size):
super().__init__(pos_x, pos_y, size, size)
class SquareButton(SingleDimensionMixin, Button):
pass
b = SquareButton(10, 20, 200)
なぜなら、SingleDimensionMixinクラスをGraphicalEntityから派生した他のクラスに適用して、1つのサイズしか受け付けないようにすることができるからです。
mixin の位置は、super()がMROに従うために重要であることに注意してください。その通り、SquareButtonのMROは(SquareButton, SingleDimensionMixin, Button, GraphicalEntity, object)なので、インスタンスを作成すると、__init__() メソッドはSingleDimensionMixinによって提供され、SingleDimensionMixinはsuper()を介してButtonの__init__()メソッドを呼び出しています。SingleDimensionMixin の super().__init__(pos_x, pos_y, size, size)という呼び出しと、Buttonのdef __init__(self, pos_x, pos_y, size_x, size_y): というシグネチャが一致しているので、すべてがうまくいきます。
もし、SquareButtonを次のように定義すると
code: python
class SquareButton(Button, SingleDimensionMixin):
pass
まずButtonが__init__()メソッドを提供し、そのsuper()がSingleDimensionMixinの__init__()メソッドを呼び出すことになります。しかし、Buttonのsuper().__init__(pos_x, pos_y, size_x, size_y)の呼び出しがSingleDimensionMixinのシグネチャdef __init__(self, pos_x, pos_y, size): と一致しないので、これはエラーになります。
mixin は、オブジェクトのインターフェイスを変更したいときだけに使われるわけではありません。super()を活用すると、次のような面白いデザインを実現できます。
code: python
class GraphicalEntity:
def __init__(self, pos_x, pos_y, size_x, size_y):
self.pos_x = pos_x
self.pos_y = pos_y
self.size_x = size_x
self.size_y = size_y
class Button(GraphicalEntity):
def __init__(self, pos_x, pos_y, size_x, size_y):
super().__init__(pos_x, pos_y, size_x, size_y)
self.status = False
def toggle(self):
self.status = not self.status
class LimitSizeMixin:
def __init__(self, pos_x, pos_y, size_x, size_y):
size_x = min(size_x, 500)
size_y = min(size_y, 400)
super().__init__(pos_x, pos_y, size_x, size_y)
class LimitSizeButton(LimitSizeMixin, Button):
pass
b = LimitSizeButton(10, 20, 2000, 1000)
print(b.size_x)
print(b.size_y)
ここで、MROまたはLimitSizeButtonは(<class '__main__.LimitSizeButton'>, <class '__main__.LimitSizeMixin'>, <class '__main__.Button'>, <class '__main__. GraphicalEntity'>, <class 'object'>) となっており、初期化の際にはまずLimitSizeMixinが__init__()メソッドを提供し、それがsuper()を介してButtonの__init__()メソッドを呼び出し、後者を介してGraphicalEntityの__init__()メソッドを呼び出すということになります。
Pythonでは、親のメソッドの実装を強制的に呼び出すことはありませんので、新しいオブジェクトのビジネスロジックの要件であれば、ここでのmixinはディスパッチ機構を停止することもできることを覚えておいてください。
実例です。Django のクラスベースのビュー
最後に、この記事のインスピレーションの元となった Django のコードベースの話をしましょう。ここでは、Django のプログラマがコードの再利用を促進するために多重継承やミキシンクラスをどのように使っているかを紹介し、その背景にある理由をすべて把握していただきたいと思います。
ご存知のように、Django の View クラスは全てのクラスベースのビューの祖先であり、HTTP リクエストメソッドを Python の関数呼び出し (CODE) に変換するディスパッチメソッドを提供しています。さて、TemplateView は、GET リクエストに答えて、ビューが呼ばれたときに渡されたコンテキス トから得られるデータを持つテンプレートをレンダリングするビューです。Django のビューの仕組みを考えると、TemplateView は get() メソッドを実装して、HTTP レスポンスの内容を返さなければなりません。このクラスのコードは code: python
class TemplateView(TemplateResponseMixin, ContextMixin, View):
"""
Render a template. Pass keyword arguments from the URLconf to the context.
"""
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
return self.render_to_response(context)
TemplateViewはViewですが、機能を注入するために2つのMixinを使用していることがお分かりいただけると思います。それでは、TemplateResponseMixinを見てみましょう。
code: python
class TemplateResponseMixin:
def render_to_response(self, context, **response_kwargs):
def get_template_names(self):
[今回の議論では重要ではないので、クラスのコードは削除しました。]
TemplateResponseMixinは、get_template_names()とrender_to_response()の2つのメソッドを任意のクラスに追加するだけであることは明らかです。後者はTemplateViewのget()メソッドの中で呼び出され、レスポンスを作成します。呼び出しの簡略化したスキーマを見てみましょう。
GET request --> TemplateView.dispatch --> View.dispatch --> TemplateView.get --> TemplateResponseMixin.render_to_response
複雑に見えるかもしれませんが、何度かコードを追ってみると全体像が見えてくると思います。例えば、DetailView (CODE)は、TemplateResponseMixinを継承したSingleObjectTemplateResponseMixinのメソッドget_template_names (CODE)をオーバーライドすることにより、単一のオブジェクトの詳細を表示する機能を備えています。 前に説明したように、Mixinはあまり汎用的であってはならず、ここでは特定のクラスで動作するように設計されたMixinの良い例を見てみましょう。TemplateResponseMixinは、self.request(CODE)を含むクラスに適用されなければなりません。これは、Viewから派生したクラスだけを意味するわけではありませんが、特定のタイプを補強するために設計されていることは明らかです。 考察のポイント
継承はコードの再利用を促進するように設計されているが、逆の結果を招くこともある
多重継承は、継承ツリーをシンプルに保つことができます。
多重継承は、PythonではMROによって解決される問題を引き起こす可能性があります。
インターフェイス(暗黙的または明示的)は設計の一部であるべきです。
Mixinクラスはクラスに簡単な変更を加えるために使われる
MixinはPythonでは多重継承を使って実装されています。表現力は高いですが、慎重な設計が必要です。
最後に
この記事を読んで、多重継承の仕組みを少しでも理解し、多重継承を怖がらなくて済むようになったなら幸いです。また、クラスは慎重に設計しなければならず、クラスシステムを作成する際には多くのことを考慮しなければならないことをお伝えできたと思います。もう一度言いますが、合成を忘れないでください。
Digging up Django class-based views series