[Python入門]リストを継承してスタックを作成するPython入門(2/2 ページ)

» 2019年08月20日 05時00分 公開
[かわさきしんじDeep Insider編集部]
前のページへ 1|2       

スタックのカスタマイズ

 このようにして作成したスタックは、必要に応じて、メソッドや特殊メソッドをオーバーライドして、その動作を自分のクラスに適したものに変更できる。以下では幾つか例を見てみよう。

__init__メソッドのオーバーライド

 例えば、__init__メソッドのオーバーライドについて考えてみよう。

 list関数は「反復可能オブジェクトを0個または1個だけ引数に取る」のが仕様だ。

mylist = list([1, 2, 3])  # 1、2、3を要素とするリストを生成
print(mylist)
mylist = list(1, 2, 3# エラー(引数は0個か1個だけ)

list関数の振る舞い

 現在のMyStack3クラスでは__init__メソッドを定義していないので、そのインスタンスを生成する際には基底クラスの__init__メソッドが呼び出され(て、そこにMyStack関数呼び出しに渡した引数が渡され)る。

mystack = MyStack3([1, 2, 3])  # 1、2、3を要素とするスタックを生成
print(mystack)
mystack = MyStack(1, 2, 3# エラー(引数は0個か1個だけ)

MyStackクラスのインスタンス生成時の振る舞い

 だが、前々回に作成したスタックでは、そのインスタンス生成時に次のような振る舞いをしていた。

  • 任意の数の引数をカンマ区切りで渡す
  • カンマで区切られた引数はそれぞれが個別にスタックの要素となる

 「スタックはリスト(の一種)」であることや、tuple関数やset関数などでも反復可能オブジェクトを受け取り、そこから要素を反復的に取り出すのが一般的であるため、現在の振る舞いの方が「よりPythonのオブジェクトらしい」といえるが、ここでは上記の振る舞いとなるように__init__メソッドをオーバーライドしてみよう。

 といっても書かなければならないコードはシンプルだ。

class MyStack3(list):
    def __init__(self, *args):
        # print(args)  # 可変長位置引数を確認したければコメントアウト
        super().__init__(args)
    def push(self, item):
        self.append(item)

インスタンス生成時の振る舞いが前々回と変わらないように修正したコード

 __init__メソッドには可変長引数を受け取るパラメーターを持たせる。このパラメーターには0個以上の引数が「タプル」にまとめられて渡される。以下の表は「MyStack3(……)」呼び出しの引数によってこのパラメーターがどうなるかをまとめたものだ。

MyStack3()呼び出し パラメーターargsの値
MyStack3() ():空のタプル
MyStack3(1) (1,):要素が1つのタプル
MyStack3([1, 2, 3]) ([1, 2, 3],):要素が1つのタプル
MyStack3(1, 2, 3) (1, 2, 3):要素が3つのタプル
MyStack3関数呼び出しの形式とargs

 パラメーターargsはタプル、すなわち反復可能オブジェクトなので、実はこれをそのまま基底クラスの__init__メソッドに渡せば、その要素を使ってリストが作成される。「MyStack3([1, 2, 3])」という呼び出しならタプルには要素が1つだけなので、「[1, 2, 3]」というリストがそのままスタックの要素となるし、「MyStack3(1, 2, 3)」ならタプルの要素は3つあるので、1、2、3を個々の要素とするスタックが作られるようになるわけだ。

 実際に試してみよう。

mystack = MyStack3()
print(mystack)
mystack = MyStack3(1)
print(mystack)
mystack = MyStack3([1, 2, 3])
print(mystack)
mystack = MyStack3(1, 2, 3)
print(mystack)
mystack = MyStack3(1, 2, [3, 4])
print(mystack)

変更したMyStack3クラスの動作を確認するコード

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

実行結果 実行結果

 実行結果からは前々回に作成したスタックと同様な振る舞いになっていることが分かる。

copyメソッドのオーバーライド

 先ほど、dir関数で3つのスタッククラスにどんな属性(メソッド)があるかを調べたときに、MyStack3クラスにはlistクラスから継承したcopyメソッドがあったことに気が付いただろうか。listクラスのcopyメソッドは「リストの新しいコピー」を戻り値とする。つまり、MyStack3クラスのインスタンスに対してcopyメソッドを呼び出しても、その戻り値はリストになってしまうのだ。実際に試してみよう。

mystack2 = mystack.copy()
print(type(mystack2))

スタックのコピーがリストになる?

 実行結果を以下に示す。

実行結果 実行結果

 上の画像に示した通り、コピーしたものの型が「<class 'list'>」になっている。スタックをコピーしたらスタックが返されるのが期待している動作ではないだろう。そこでMyStack3クラスのインスタンスが返されるようにしてみよう。

 これも実際のコードは簡単だ。

class MyStack3(list):
    def __init__(self, *args):
        # print(args)
        super().__init__(args)
    def push(self, item):
        self.append(item)
    def copy(self):
        tmp = list.copy(self)
        return MyStack3(*tmp)

MyStack3クラスのインスタンスを返すようにcopyメソッドを変更したコード

 ここでは、listクラスが持つcopyインスタンスメソッドを「list.copy(self)」のような形で呼び出して、まずはスタックに格納されたデータのコピーを取り出している。これはリストなので、それをMyStack3関数に渡すことで、新しいMyStack3クラスのインスタンスを呼び出している。ただし、__init__メソッドを「単一のリスト(反復可能オブジェクト)を渡したら、それが単一の要素としてスタックの初期値となる」ようにオーバーライドしているので、ここでは「*」を使ってリストの要素が展開されたものが__init__メソッドに渡されるようにしている点に注意しよう(こうした面倒くさい処理が出てくるのも通常のPythonのオブジェクトとは異なる振る舞いをするようにしているからだろう)。

 もう一つ注意したいのは「list.copy(self)」という書き方だ。これは上でも述べたように「listクラスのcopyインスタンスメソッドを呼び出し」て、その引数に自分自身(self)を渡すという意味だ。なぜ「self.copy()」のように書かないかというと、現在オーバーライドしているコード自体が「self.copy」インスタンスメソッドだからだ。つまり、「self.copy()」とすると、オーバーライドしているcopyメソッドの内部でそのメソッド自身をさらに呼び出すコードになってしまう(いわゆる無限ループになる)。ここでしたいことは、「基底クラスであるlistクラスが持つcopyインスタンスメソッドを呼び出して、その結果を利用する」ことなので、このような記述をしている。

 同時に、インスタンスメソッドは「インスタンス名.インスタンスメソッド名(引数)」のような形式だけではなく、「クラス名.インスタンスメソッド名(処理対象のインスタンス, 引数)」のような形式でも呼び出せることは覚えておこう(このような書き方をすることはそれほど多くはないが、インスタンスメソッドの第1パラメーターが「self」となっていることを考えると、何となくそういうものだと感じられるはずだ)。

 では、このコードの動作を確認してみよう。

mystack = MyStack3(1, 2, [3, 4])
print(mystack)
mystack2 = mystack.copy()
print(type(mystack2))
print('mystack:', mystack)
print('mystack2:', mystack2)
print('mystack == mystack2:', mystack == mystack2)
print('mystack is mystack2:', mystack is mystack2)

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

 実行結果を以下に示す。

実行結果 実行結果

 2行目の出力結果を見ると、copyメソッドの戻り値の型がMyStack3になったことが分かる。また、両者は同じ要素を持ち、==演算子で等価性を比較するとその結果はTrueに、is演算子でオブジェクトの同一性を比較するとその結果がFalseになることから、両者が同じ要素を持つ別のオブジェクトであることも分かる(これはcopyメソッドに求められる動作である)。

 このように、クラスを継承しても、基底クラスの全てが自分が作成しているクラスで適切な振る舞いをするとは限らない。必要に応じて、自分でその動作や振る舞いをカスタマイズする必要がある。とはいえ、ひな型となるメソッドは得られるので、多くの場合はそれほど難しいコードにはならないだろう。興味のある方はその他のメソッドも自分でオーバーライドしながら挙動を確認してみよう。

まとめ

 今回は以前に作成したスタックを、リストを継承することでもう一度作成してみた。その過程で「is-a」「has-a」の関係などについて見た後で、メソッドをオーバーライドすることで、基底クラスの動作を変更する方法についても見た。次回は、クラスのメンバと継承について見ていく予定だ。

今回のまとめ:リストを継承してスタックを作成する

  • クラスを継承することで、派生クラスでは基底クラスが持つ特性を受け継ぎ、それらを利用できる
  • 継承は「is-a」の関係を表すために使われる
  • あるクラスで構成部品として他のクラスを利用することを「has-a」の関係や「包含」などと呼ぶ
  • クラスの継承を行った後には、派生クラスではさまざまなメソッドをオーバーライドして、基底クラスの動作を変更できる
  • インスタンスメソッドは「クラス.インスタンスメソッド名(対象のオブジェクト, 引数)」のようにしても呼び出せる

前のページへ 1|2       

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。