以前にリストを利用して作成したスタックを、今度はリストを継承して作成しながら、「is-a」「has-a」「クラスのカスタマイズ」といったことについて見ていこう。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
前回はクラスの継承に関する基礎知識を紹介した。そこで、今回は前々回に見たスタックをクラスの継承を利用して作成してみよう。
前々回に作成したスタックのコードは、内部でリストを利用している次のようなものだった(初期バージョン)。
class MyStack:
def __init__(self):
self.stack = []
def push(self, item):
self.stack.append(item)
def pop(self):
return self.stack.pop()
このスタックは、オブジェクトのプッシュとポップは可能だが、インデックスアクセスをしたり、文字列表現を得たりといった「Pythonのオブジェクト」らしい振る舞いを実現するには、自分で特殊メソッドを定義(オーバーライド)する必要があった。これを行ったのが以下のコードだ。便宜上、クラス名を「MyStack2」に変更してある。
class MyStack2:
def __init__(self, *args):
self.stack = []
for item in args:
self.stack.append(item)
def push(self, item):
self.stack.append(item)
def pop(self):
if len(self.stack) == 0:
return None
return self.stack.pop()
def __repr__(self):
return 'MyStack(' + repr(self.stack) + ')'
def __str__(self):
return str(self.stack)
def __iter__(self):
return iter(self.stack)
def __getitem__(self, key):
return self.stack[key]
このコードでは幾つかの特殊メソッドを定義することで、インスタンス生成時の初期値の設定、公式/非公式の文字列表現の取得、インデックスアクセス、反復可能オブジェクトとしての振る舞いを実現している(加えて、空のスタックに対するポップ操作では例外とせずに、None値を返すようにもなっている)。
では、リスト(listクラス)を継承して、スタックを実装するとどうなるかを見てみよう。最もシンプルなコードは次のようになる。クラス名は「MyStack3」としている。
class MyStack3(list):
def push(self, item):
self.append(item)
わずかこれだけのコードとなる。これで前々回に作成したスタックと同様な処理が可能かを試してみよう。
mystack = MyStack3()
mystack.push(1)
mystack.push(2)
mystack.push(3)
mystack.push(4)
mystack.push(5)
print(mystack)
print(mystack.pop())
print(mystack.pop())
for item in mystack:
print(item)
print(mystack[1:])
このコードを実行すると次のようになる。
おおむね前回と同様な結果となっている。スタックに対するプッシュ/ポップ操作も「後入れ先出し」(LIFO)になっているし、for文で反復処理も行えている。
では、以下に再掲するほんのわずかなコードでスタックを作成できたのはなぜかについて少し考えてみよう。
class MyStack3(list):
def push(self, item):
self.append(item)
MyStack3クラスの定義では、基底クラスとして「list」クラスを指定して、pushメソッドを定義しているだけだ。pushメソッドでは「self.append(item)」のようにしているが、これは「MyStack3クラスのインスタンス(self)に対してappendインスタンスメソッドを呼び出して、itemを追加する」ことを意味している。クラス定義にはappendメソッドはないが、こうした記述ができるのは、もちろん基底クラスであるlistクラスからappendメソッドを継承しているからだ。インデックス指定や繰り返し処理が可能なのも同じ理由からだ。
このことを確認するために、MyStack、MyStack2、MyStack3の各クラスで定義されている属性(メソッド)を確認してみよう。これにはdir関数に各クラスを渡して、その結果をprint関数で表示するだけでよい。
print('MyStack:', dir(MyStack))
print('---')
print('MyStack2:', dir(MyStack2))
print('---')
print('MyStack3:', dir(MyStack3))
実際に実行した結果を以下に示す。
MyStackクラスは特殊メソッドを定義せず、pushメソッドとpopメソッドだけを定義している(最後にこれら2つのメソッドが表示されている)。これら以外に、「dir(MyStack)」呼び出しの結果で得られているのは、暗黙的な基底クラスであるobjectクラスから継承したものだ。
MyStack2クラスでは、これらに加えて、「__iter__」「__getitem__」メソッドが増えている(「__init__」「__repr__」「__str__」メソッドはobjectクラスにある同名メソッドを上書きしている)。
MyStack3クラスの出力を見ると、書いたコードの量はごくわずかなのに、MyStack2クラスよりもさらに多くの属性(メソッド)が表示されていることが分かるはずだ。これがlistクラスを継承した結果だ。
前回も述べたように、「継承」とは「is-a」の関係をプログラムコードで記述/実現するための仕組みであり、この場合、「スタックはリスト(の一種)」であると上のコードでは定義して、その基底クラスであるリストが持つ特性を利用できるようにしている。そのため、MyStack3クラスのインスタンスを生成すれば、それはリストのようにも扱える(リストが持つデータ操作機能を呼び出せる)し、MyStack3クラスで定義されているプッシュ操作(pushメソッド)も呼び出せる。
これに対して、前々回のスタックのコードでは、内部にリストを持ち、そのリストを操作するためのインタフェースとなる特殊メソッドを自分で定義していた。このように内部で何らかのオブジェクトを持つことを「has-a」の関係という(「継承」と対照的に「包含」という言葉で表されることもある)。この場合は、「スタックはリストを包含する」となる。これはあるクラスの構成部品として別のクラスを利用することを意味している。
今回はリストを継承してスタックを作成しているが、「スタックはリスト(の一種)」であるかどうかは判断が難しい。Pythonのリストにはスタック的にも使えるようにpopメソッドが用意されていることからも「スタックはリスト(の一種)」といえるという人もいるだろう。そうであれば、リストを継承することで、さまざまな特性を受け継ぎ、コードの記述の手間が大幅に省けるだろう。
対して、「スタックはリストを包含する」(リストはスタックの構成部品)という表現の方が適切という人もいるかもしれない。この場合はスタックをリスト的に使いたいのであれば、前々回に見たようにさまざまなメソッドを自分で書く必要がある。その一方で、リストを継承することで、リストには適切でもスタックでは不要な特性を受け継ぐことはない。必要なものだけを選択的にスタックに持たせられる。
自分でクラスを定義する際に、どのような形で(継承か、包含かなど)他のクラスを利用するかは、自分が解決しようとしている問題をどのように分析し、必要なものが何か、それをどう組み合わせればよいかで決まってくることだろう。
Copyright© Digital Advantage Corp. All Rights Reserved.