検索
連載

[Python入門]多重継承とmixinPython入門(1/2 ページ)

多重継承を行う際には複数のクラスからインスタンス変数を継承すると問題が発生する。それを回避する方法と、そこから生まれるmixinという考え方を紹介する。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
「Python入門」のインデックス

連載目次

 前回はPythonでのクラスの多重継承と、多重継承したクラスでメソッドを呼び出したときに、それらがどのようにして解決されるかについて見た。今回は多重継承時に留意すべきもう一つの点について考えていく。

インスタンス変数の共有にまつわる問題

 ここでは次のような2つのクラスの動作を確認した後に、これらを継承して、スタックのようにもキューのようにも振る舞うクラスを作ってみよう。

class Stack:
    def __init__(self):
        self.x = []
    def push(self, item):
        self.x.append(item)
    def pop(self):
        return self.x.pop()

class Queue:
    def __init__(self):
        self.x = []
    def enqueue(self, item):
        self.x.insert(0, item)
    def dequeue(self):
        return self.x.pop()

スタックとしてもキューとしても使えるStackQueueクラス

 StackクラスとQueueクラスは第30回「クラスを使ってスタックとキューを作成する」で紹介したコードを簡略化したものだ。ただし、Queueクラスについては、項目を追加(エンキュー)するときには内部で使っているリストの先頭に挿入して(insertメソッドを使用)、項目を取り出す(デキュー)ときにはリストが持つpopメソッドを使うようにしている(この時点で先が見えたという方もいらっしゃるだろう)。

 これらのクラスの動作を確認してみよう。

mystack = Stack()
mystack.push(1)
mystack.push(2)
print(mystack.pop())
print(mystack.pop())

myqueue = Queue()
myqueue.enqueue(1)
myqueue.enqueue(2)
print(myqueue.dequeue())
print(myqueue.dequeue())

StackクラスとQueueクラスの動作を確認するコード

 これらのコードを実行すると、次のようになる。

実行結果
実行結果

 想定通りにスタックとキューとして振る舞っていることが分かる。では、これらを多重継承したStackQueueクラスを定義してみよう。

class StackQueue(Stack, Queue):
    pass

StackクラスとQueueクラスを継承するStackQueueクラス

 StackQueueクラスは「Stackでもあり、Queueでもある」クラスだ。「is-a」の関係は成り立っていると考えてもよいだろう。だが、実際の動作はどうだろう。

mysq = StackQueue()
mysq.push(1)
mysq.push(2)
mysq.enqueue(3)
mysq.enqueue(4)
print(mysq.dequeue())  # 最初に入れたデータは「1」が取り出されるか
print(mysq.pop())  # 最後に入れたデータは「4」が取り出されるか

StackQueueクラスの動作を確認するコード

 このコードを実行すると、次のようになる。

実行結果
実行結果

 コメントとは異なる結果になっている。何が起こっているのかというと、StackクラスのpopメソッドとQueueクラスのdequeueメソッドはともに内部で使用しているリストの末尾の要素を取り出して、戻り値にしているということになる。さらに、Stackクラスから受け継いだpushメソッドはリストの末尾にデータを追加して、Queueクラスから受け継いだenqueueメソッドはリストの先頭にデータを挿入している。

StackQueueクラスの動作
StackQueueクラスの動作

 そのため、dequeueメソッドを実行しても、popメソッドを実行しても想定とは異なる値が取り出されたということだ。

 こうなってしまう一番の理由はStackクラスとQueueクラスが内部的に使用しているリストの扱いが異なる(Stackは末尾にデータを追加していき、Queueは先頭にデータを挿入していく)からだと思うかもしれない。だが、これらのクラスは単体では問題なく動作をしていたのも既に見た通りだ。StackクラスとQueueクラスを別々の人が定義していたのであれば、単体では正常に動作するのだから、どちらに責任があるかを問えるはずもない。

 では何が一番の問題かといえば、「多重継承によって、扱い方の異なる、同名のインスタンス変数を共有してしまった」ことだ(そもそも、インスタンス変数xはどこで初期化されているのだろう。「StackQueue.__mro__」などでMROがどうなっているかを調べれば、すぐに答えが出るはずだ)。

 このようにむやみやたらに多重継承を行うと、複数の基底クラスでインスタンス変数を定義している場合に、多重継承したクラスでそれらが問題を引き起こす可能性がある。よって、多重継承を行う際には、それらの扱いについて注意深く考える必要がある。

 あるいは、「インスタンス変数を含むクラスを継承するのは1つだけ」とするルールを自らに課す方法もある。その場合、追加で継承できるのは「メソッドのみを含んだクラス」となる。そうすることで、インスタンス変数の共有によって生じる問題を回避できるようになる。

mixin

 今述べたような、インスタンス変数を継承するクラスは1つだけ、他のクラスはメソッドだけとする多重継承の方法についてもう少し考えてみよう。

 この場合、メソッドだけが定義されているクラスとは「何らかの機能だけを持つクラス」ともいえる。プログラムとは「データとそれを操作するための機能」で構成されるが、そのうちの「機能」だけを持つクラスといえる。本来、クラスとはデータと機能をまとめたものだが、多重継承で複数継承してもよいのはこのような機能だけが定義されたクラスということだ。

 このような多重継承の使い方のことを「mixin」(ミックスイン、ミキシン)と呼ぶ。「mixin」とは「幅広いクラスで共通して使われる機能(だけ)を、継承(や他のプログラミング言語では他の方法)を使って組み込む(ミックスさせる)」ことといえる。以下ではmixinされる側のクラスのことを「mixinクラス」と呼ぶことにしよう。

 「継承」が「基底クラスの機能を受け継ぎながら、必要に応じて、その振る舞いを変更したり、新たな機能を追加したりすることでより特殊な(具体的な)クラスへと仕立てる」ことに対して、「ミックスイン」は「既にある機能を、親子関係にない複数のクラスで共通に利用する」ものと考えられる。

mixin
mixin

 と小難しいことをいうよりは、コードを見てもらった方がよいだろう。例えば、プログラムを書く際には、MRO(メソッド解決順序を調べたいとか、現在のインスタンスの値を一覧したいといったことがあるかもしれない。そうした機能をUtilクラスにメソッドとしてまとめて、それらをmixinして使ってみよう(MROは「クラス名.__mro__」で調べられるので必要ないといえば必要ないのだが、サンプルとして使ってみる)。

 実際のコードを以下に示す。Utilクラスはmixinされるクラス(mixinクラス)である。

class Util:
    def show_members(self):
        print(self.__dict__)
    def show_mro(self):
        print(self.__class__.__mro__)

インスタンスが持つインスタンス変数の一覧とMROを表示するメソッドをまとめたUtilクラス

 Utilクラスは多重継承でmixinされることを念頭に置いている。Utilクラスに__init__メソッドがないのは、mixinされるクラスでは上で述べたようにインスタンス変数を持たず、その初期化も必要ないからだ。ただし、このクラスのインスタンスを作っても、あまりに役には立たない。というのは、プログラム開発の途中であればともかく、ここで定義しているメソッドは他のクラスで利用されることを前提としたものだからだ(__init__メソッドでインスタンス変数を初期化しているわけでもないので、実際、show_membersメソッドを実行する意味はないだろう)。

 それ以外のメソッドの定義の方法は、これまでに見たものと同じだ。第1パラメーターはselfで、必要に応じて、メソッド呼び出しで必要とするデータを受け取るパラメーターを並べていく。メソッドの本体も通常のメソッドと同じく、selfなどを利用して、何らかの処理を行い、必要なら値を返せばよい。

 ここでは、show_membersメソッドでインスタンスが持つ__dict__属性を表示し、show_mroメソッドでそのインスタンスが属するクラスのMRO(self.__class__.__mro__)を表示している。前者の__dict__属性には、インスタンスが持っているインスタンス変数とその値が辞書形式で保存されている。

 では、このクラスをmixinしてみよう。

class Foo:
    def __init__(self):
        self.foo = 'FOO'

class Bar(Foo, Util):
    def __init__(self):
        super().__init__()
        self.bar = 'BAR'

Fooクラスを継承し、UtilクラスをmixinするBarクラス

 ここではBarクラスがFooクラスを継承し、Utilクラスをmixinしている。

Foo/Bar/Utilクラスの継承階層
Foo/Bar/Utilクラスの継承階層

 __init__メソッドではインスタンス変数fooとbarが初期化されている。では、この動作を確認してみよう。

bar = Bar()
bar.x = 100
bar.show_members()

Barクラスの動作を確認するコード

 Barクラスのインスタンスbarを作成すれば、FooクラスのメソッドやUtilクラスのメソッドを呼び出せるようになる(もちろん、selfにはBarクラスのインスタンス自身が渡される)。また、上のコードでは、barに属性x(インスタンス変数)を追加していることにも注意しよう。これを実行すると次のようになる。

実行結果
実行結果

 インスタンスbarが持つ、3つの属性(インスタンス変数)が表示されたことが分かるはずだ。

 Utilクラスは、Foo→Barという継承階層からは独立したユーティリティークラスなので、例えば、次のように別のクラス階層を構成するクラスであっても自由に利用できる(この動作を確認するコードやその実行結果は省略する)。

class Base:
    pass

class Derived(Base, Util):
    def __init__(self):
        self.some_value = 100

Utilクラスは他のクラス階層にもミックスインさせることが可能

Copyright© Digital Advantage Corp. All Rights Reserved.

       | 次のページへ
[an error occurred while processing this directive]
ページトップに戻る