連載
» 2023年11月10日 05時00分 公開

[Python入門]クラスを使ってスタックとキューを作成するPython入門(2/2 ページ)

[かわさきしんじDeep Insider編集部]
前のページへ 1|2       

スタックをより使いやすくする

 ここまでにスタックとキューをクラスとして定義できた。

 だが、これら2つのクラスはより使いやすくできる。例えば、次のような処理が可能なクラスとすることが可能だ。

  • インスタンスの生成時に、初期値を与える
  • print関数などに、このクラスインスタンスを渡したときにその要素が表示されるようにする
  • for文と組み合わせて使用する
  • インデックス指定により、特定の要素を取得する

 これらの処理を可能にすることで、Pythonの組み込み型(リストなど)と似た使い勝手になるはずだ。そして、これを実現するためには「特殊メソッド」と呼ばれる、Pythonで特別扱いされているメソッドをクラス内で定義する。ここではスタックを例に、これらの処理を実現してみよう。

特殊メソッド

 特殊メソッドとは、自分が作成したクラスを、Pythonが提供しているさまざまな演算子や関数呼び出し、構文の中でも使えるようにするためのものだ。例えば、先ほど定義したMyStack関数のインスタンスをprint関数に渡してみよう。

mystack = MyStack()
print(str(mystack))
print(repr(mystack))

MyStackクラスのインスタンスの文字列表現を取得する

 これを実行すると、次のような表示になる。

実行結果。同じ出力が得られた 実行結果。同じ出力が得られた

 最初の出力はstr関数のもので、次の出力はrepr関数のものだ。第26回「オブジェクトの同一性、比較、文字列表現」の「str関数とrepr関数」でも述べたが、str関数はオブジェクトの「非公式な文字列表現」を得るもので、repr関数はオブジェクトの「公式な文字列表現」を得るものだ。ここで2つの出力が一緒なのは、MyStackクラスには非公式な文字列表現を得る手段が用意されていないので、その公式な文字列表現が代わりに使われているからだ*2

*2 もう少し詳しくいえば、MyStackクラスでは公式な文字列表現と非公式な文字列表現を得る手段がどちらもないので、元となるobjectクラスで定義されているものが使われて、最終的に既定のフォーマットを使用した公式の文字列表現を得ている。


 そこで、公式な文字列表現と非公式な文字列表現を得る手段を、MyStackクラスに用意すると、上と同じコードを実行したときに次のような出力が得られるようになる。

実行結果。両者の出力が異なることに注意 実行結果。両者の出力が異なることに注意

 ここで「オブジェクトの公式な文字列表現」(2つ目の出力)は「その文字列表現からもともとのオブジェクトを復元できる」ことが望ましい。そのため、上の出力では「MyStack([])」とMyStackのインスタンスを作成するための「MyStack()」呼び出しに引数として空の文字列を渡したものになっている(ただし、これまでに定義したMyStackクラスではMyStack()呼び出しに引数を渡せない。これについてはこの後、対応する)。対して、「オブジェクトの非公式な文字列表現」(1つ目の出力)については単にリストの内容を表示するようにしたので、「[]」とだけ表示されている。

 そして、あるクラスのインスタンス(オブジェクト)を文字列化する際に使われるのが、__repr__メソッドと__str__メソッドの2つのメソッドだ。特殊メソッドの名前は「repr」「str」など、それがどんな目的で使われるかを示す単語を二重のアンダースコア「__」で囲んだものになる。既におなじみの「__init__」メソッドもそうした特殊メソッドの一つであり、「インスタンスの初期化を行う」ためにPythonが必要に応じて自動的に呼び出してくれるものだ。

 特殊メソッドがどんなものかがおおよそ分かったところで、以下では上に挙げたさまざまな処理をMyStackクラスに備えていこう。

インスタンスの生成時に、初期値を与える

 MyStackクラスのインスタンス生成時に、その初期値を与えると、それを要素とするスタックが作成されると便利かもしれない。また、上述したように、repr関数が返す公式な文字列表現が「MyStack([…])」のように中にスタックの要素を含むものとなっているのであれば、そこからスタックを復元できる必要もある。これには、既に定義している__init__メソッドを変更して、MyStack()呼び出しで引数を指定できるようにすればよい。

 ただし、__init__メソッドの定義を修正する前に考えておくことがある。それは引数の受け渡しをどのように行い、それらを基にスタックの初期値をどのように設定すればよいかだ。

 幾つかの方法が考えられるが、ここでは次のようにすることにした。

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

 これを実現するには、可変長位置引数を使えばよい。__init__メソッドにカンマで区切って渡される引数は「*」を付加したパラメーター「*args」で引き受ける。後は、それをリストに追加するだけだ。

 実際のコードは次のようになる。

class MyStack:
    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()

スタックの初期値を設定するように修正したコード

 ここではfor文でargsの要素を一つ一つリストに追加するようにしたが、実際には「self.list.extend(args)」という1行で処理できるので、興味のある方はそちらのコードも試してみよう(むしろ、簡潔という意味ではこちらの方が望ましいだろう)。

 では、これまでと同様に、この動作を試してみよう。

mystack = MyStack(1, 2, [3, 4])
print(mystack.stack)

スタックの初期値が設定されているかを確認するコード

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

実行結果 実行結果

 出力を見ると、カンマで区切った「1」「2」「[3, 4]」がそのままスタックの要素となったことが分かる。

print関数などに、インスタンスを渡したときにその要素が表示されるようにする

 次に「print関数などに、インスタンスを渡したときにその要素が表示されるようにする」方法だ。同時に、repr関数で公式な文字列表現を得る方法についても見ておこう。

 これらには次の2つのメソッドを利用する。

  • __str__(self)メソッド:オブジェクトの非公式な文字列表現を返すようにする。print関数やstr関数、format関数にオブジェクトを渡すと、そのクラスのこのメソッドが呼び出される。クラスで__str__メソッドが定義されていないと、基となるクラスの__str__メソッドが呼び出される
  • __repr__(self)メソッド:オブジェクトの公式な文字列表現(それを復元可能な文字列表現または山かっこ「<>」内に有用な情報を含めたもの)を返すようにする。repr関数にオブジェクトを渡すと、そのクラスのこのメソッドが呼び出される。さらにオブジェクトの非公式な文字列表現を得る手段が用意されていない(つまり、__str__メソッドを定義していない)ときには、print関数やstr関数にオブジェクトを渡すと、最終的にこのメソッドが呼び出されるようになっている

 これら2つのメソッドを定義すればよいのだが、やはりその前に考えておくべきことがある。それはどのような表現がMyStackオブジェクトの文字列表現として正しいかだ。

 公式な文字列表現については、既に述べたように「元のオブジェクトを復元可能」な表現にすればよく、上で__init__メソッドがカンマ区切りでスタックの初期値を受け取れるようにしたので、「MyStack(カンマ区切りの要素)」という表現を戻り値にすればよいだろう。そこでまずは、これを定義してみよう。

class MyStack:
    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) + ')'

オブジェクトの公式な文字列表現を返すようにしたMyStackクラス

 単にスタックの入れ物としているリストをrepr関数で文字列化したものを「'MyStack(」と「)」で挟み込んだ文字列を作成するだけだ。reprメソッドは公式な文字列表現を作成することが目的なので、そのインスタンス変数の文字列表現を得るにはstr関数で非公式なものを得るよりも、repr関数を使って公式なものを得るのが適切だろう。

 では、動作を確認してみよう。

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

公式な文字列表現の取得を確認するコード

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

実行結果 実行結果

 どちらの実行結果も「MyStack(1, 2, [3, 4])」となった。これは、print関数やstr関数にMyStackクラスのインスタンスを渡すと、内部的にはそのクラスの__str__メソッドが呼び出されることになっているが、これがまだ定義されていない状態では最終的に__repr__メソッドが呼び出されるためだ(これにより、あるクラスの公式な文字列表現が、その非公式な文字列表現の代替として使われるようになる)。

 では、__str__メソッドを定義しよう。ここでは、スタックの要素すなわちリストの要素を表示するだけなので、その文字列表現を得ればよいだろう。よって、コードは次のようになる。

class MyStack:
    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)

オブジェクトの非公式な文字列表現を返すようにしたMyStackクラス

 ここでは、str関数を使って、内部で使用しているインスタンス変数stackの非公式な文字列表現を得て、それを戻り値としている。

 では、先ほどと同じコードで動作を確認してみよう。

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

公式な文字列表現と非公式な文字列表現の取得を確認するコード

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

実行結果。公式な文字列表現と非公式な文字列表現が異なるものを返すようになった 実行結果。公式な文字列表現と非公式な文字列表現が異なるものを返すようになった

 これにより、repr関数によって得られる公式な文字列表現と、print関数やstr関数によって得られる非公式な文字列表現が別のものになったことが分かる。

 また、公式または非公式な文字列表現を得ることで、スタックの要素を調べられるようにもなった。これにより、インスタンスの属性に「mystack.stack」のようにしてアクセスする必要がなくなることにも注意しよう。本来、インスタンス変数に外部からアクセスすることはお行儀がよくないことだ(インスタンス内部で使用することが目的のため)。不用意にインスタンスの属性に直接アクセスするよりは、このように何らかの手段でそれを調べたり、変更したりするための手段(メソッド)を用意して、クラスを利用する際にはそれらを使うようにすることが望ましい。

for文と組み合わせて使用する

 for文と組み合わせて使用する、とはつまり「MyStackクラスのインスタンスが反復可能オブジェクトとして振る舞えるようにする」ということだ。例えば、次のようなコードを実行できるようになる。

mystack = MyStack(1, 2, [3, 4])
for item in mystack:
    print(item)

反復可能オブジェクトとして振る舞えるようになれば、このコードが実行できるようになる

 こうすることで、スタックの要素を繰り返し処理で取得して、それを何らかの処理に利用できるようになる(本来、スタックは「後入れ先出し」「LIFO」なデータ構造を扱うためのものなので、こうしたことを可能にするのがよいかどうかの議論はあるだろう)。

 スタック(やリスト)のように、複数のデータを格納するオブジェクトのことを「コンテナ」と呼ぶが、コンテナが反復処理をサポートするようにするには、コンテナに__iter__メソッドを定義すればよい。このメソッドは、iter関数やfor文から呼び出されて、そのコンテナの要素を列挙する際に使われる(詳細はPythonのドキュメント「イテレータ型」を参照されたい)。

 そのため、MyStackクラスにも__iter__メソッドを定義すればよいのだが、__iter__メソッドはイテレータを返す必要がある。これをどうすればよいかが問題となるが、スタックの要素を保存するのに使っているリストをiter関数に渡すと、その要素を反復するのに使えるイテレータが得られるようになっている。そのため、MyStackクラスの__iter__メソッドではこれを返送するだけで済む。

 コードは次のように簡単だ。

class MyStack:
    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)

反復処理に対応するようにしたMyStackクラス

 たったこれだけで、反復処理が可能になる。実際に試してみよう。

mystack = MyStack(1, 2, [3, 4])
for item in mystack:
    print(item)

反復可能オブジェクトとして振る舞うかを確認するコード

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

実行結果 実行結果

 反復処理が実現できていることが分かるはずだ。

インデックス指定により、スタック中の特定の要素を取得する

 最後にインデックス指定を行って、その値を取得できるようにしてみよう。これには__getitem__メソッドを定義する。「mystak[0]」などと記述すると、このメソッドが呼び出される。呼び出されると、そのインデックス(またはキー)もメソッドに渡されるので、その値を基に適切な要素の値を返送するようにする。例えば、「mystack[0]」なら「0」が渡されるので、この場合はリストの先頭要素(self.stack[0])を返すことになるだろう。

 実際のコードは次の通りだ。

class MyStack:
    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]

インデックス指定に対応したMyStackクラス

 では以下のコードで、インデックス指定が可能かを試してみよう。

mystack = MyStack(1, 2, [3, 4])
print(mystack[0])
print(mystack[0:2])  # スライスできるか?

インデックス指定が可能かを確認するコード

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

実行結果 実行結果

 上の画像の通り、インデックス指定もスライスも可能なことが分かる。

 ただし、注意したいのは__getitem__メソッドを定義しても、できるのはインデックスやスライスによって値を取得することだけな点だ。インデックスを指定して、その値を変更するには__setitem__メソッドを、インデックスを指定して特定の要素を削除するには__delitem__メソッドを定義する必要がある。興味のある方は、これらのメソッドも自分で定義してみよう。

 さらにリストなどの要素数を得るには、len関数を使用するが、このスタックで要素数を調べることはできない。これを可能にするには、__len__メソッドを定義する必要がある。このようにコンテナやシーケンスに属するクラスを定義するときには、さらに多くのメソッドやそれを操作するための関数類を定義する必要がある。詳しくはPythonのドキュメント「コンテナをエミュレートする」を参照されたい。また、特殊メソッド全般およびどんな特殊メソッドがあるかについては同じくPythonのドキュメント「特殊メソッド名」を参照されたい。

 最後に一つ。上では__iter__メソッドを定義することで、スタックが反復可能オブジェクトとして振る舞えるようにしていたが、Pythonの用語集にある「iterable」には「反復可能オブジェクトの例には……(中略)…… Sequence 意味論を実装した __iter__() メソッドか __getitem__() メソッドを持つ任意のクラスのインスタンスが含まれます」とも書いてある。つまり、__getitem__メソッドを定義することでも、頒布可能オブジェクトとして振る舞えるようになる。興味のある方はこちらも試してみよう。

まとめ

 今回は、Pythonのクラス機構を利用して、プログラミングの世界でよく目にするスタックとキューを自分で作成し、その後、さまざまな特殊メソッドを定義することで、Pythonの組み込み型と似た使い勝手にするための基礎を見た。今回はリストをインスタンス変数として内部に持つことで、スタックやキューを実現した。しかし、反復処理を行ったり、インデックス指定を行ったりするのにわざわざインスタンス変数を操作するメソッドを定義していた。これはかなり煩雑な作業であると共に、一つ一つの作業で行うことは単純である。内部でリストを利用するのではなく、リストを基にスタックを作成することで、こうした作業の大半が必要なくなる。これを実現するのが「クラスの継承」という機構だ。次回はこれについて見ていくことにしよう。

今回のまとめ:

  • スタックは「後入れ先出し」(LIFO)なデータ構造
  • スタックにデータを置くことを「プッシュ」と、スタックからデータを取り出すことを「ポップ」と呼ぶ
  • ポップ操作により直前にスタックへ積まれたデータが取り出される
  • キューは「先入れ先出し」(FIFO)なデータ構造
  • キューにデータを置くことを「エンキュー」と、キューからデータを取り出すことを「デキュー」と呼ぶ
  • デキュー操作により、キューの先頭にある(一番古い)データが取り出される
  • Pythonではスタックとキューはリストを使って実現できる
  • クラスに特殊メソッドを定義することで、Pythonで標準的に行われている操作を、自分が定義したクラスでもサポートできるようになる
  • 特殊メソッドの名前は「repr」「str」など、それがどんな目的で使われるかを示す単語を二重のアンダースコア「__」で囲んだものになる
  • 特殊メソッドは、必要に応じてPythonから自動的に呼び出される
  • 本稿で定義した特殊メソッドを以下に一覧する
特殊メソッド 説明
__repr__(self) オブジェクトの公式な文字列表現を返す
__str__(self) オブジェクトの非公式な文字列表現を返す
__iter__(self) オブジェクトが反復可能オブジェクトとして振る舞えるように、イテレータを返す
__getitem__(self, key) パラメーターkeyで示されるインデックス位置の要素の値を返す。このメソッドを定義することで、インデックス指定やスライスが可能になる
__init__(self) インスタンスの初期化を行う。追加のパラメーターを持たせることで、インスタンスに任意の初期値を与えられるようになる
本稿で取り上げた特殊メソッド


「Python入門」のインデックス

Python入門

前のページへ 1|2       

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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