[Python入門]ジェネレータ関数とジェネレータイテレータの基礎Python入門(2/2 ページ)

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

カウントアップするジェネレータイテレータ

 クラスを使って定義したカウントアップするだけのイテレータは次のようなものだった。

class CountUpIterator:
    def __init__(self, limit=5):
        self.limit = limit
        self.counter = -1
    def __iter__(self):
        print('__iter__ method called')
        return self
    def __next__(self):
        print('__next__ method called')
        self.counter += 1
        if self.counter >= self.limit:
            raise StopIteration()
        return self.counter

カウントアップするイテレータ

 ここでは__init__メソッドでカウンターの上限値を指定して、__next__メソッドで現在値と上限値を比較して、上限に達していなければ、現在値を返して、その値を1増やすようにしていた。上限値の指定は、ジェネレータ関数のパラメーターに受け取ることにすれば、ジェネレータ関数は次のように書ける。クラスを使ったときよりもシンプルに、その動作だけを書けていることに注目しよう。

def countup_geniter(limit=5):
    counter = -1
    while True:
        counter += 1
        if counter >= limit:
            break
        yield counter

ジェネレータ関数を使って、カウントアップするイテレータを書き直したもの

 CountUpIteratorクラスでは、__next__メソッドは呼び出されるたびに、そのコードが実行されるので、現在値(self.counter)と上限値(self.limit)をインスタンス変数として管理していた。一方、ジェネレータ関数では、それらをローカル変数やパラメーターを使って現在値(counter)と上限値(limit)を管理して、それを無限ループ(「while True:」のブロック)の中で操作するようにしている。

 無限ループの中でyield式が実行されると、現在値(と実行の流れ)が呼び出し側に戻され、次のnext関数(__next__メソッド)呼び出しにより、yield式の次の行、つまり次の無限ループの先頭に移り、次のループが始まる。これにより、上限に到達するまでは、ジェネレータイテレータが連続的に値を生成(ジェネレート)するようになっている。

 現在値と上限値を比較したときに現在値が上限値に達していれば、クラスベースのCountUpIteratorでは「raise StopIteration()」として自ら例外を発生させていたが、上のコードではbreak文で無限ループを終了している(あるいは、「return」とだけ書いて、関数本体の実行を終了してもよい)。

 これは、先ほども述べたように「yield式に到達しないままコードの実行が終了したところで、StopIteration例外が発生する」からだ。自分でStopIteration例外を発生させる必要はない点には注意しよう。上のコードをbreak文から「raise StopIteration()」に変更すると、環境によっては次のようにRuntimeError例外が発生する(Python 3.7.3以降でのデフォルトの振る舞い。それ以前のバージョンでも警告メッセージが表示されるかもしれない。詳細はPEP 479「Change StopIteration handling inside generators」を参照されたい)。

 使用例を以下に示す。

for num in countup_geniter(3):
    print(num)

カウントアップするイテレータをジェネレータイテレータの使用例

 ジェネレータイテレータはもちろんイテレータなので、このようにfor文でも利用可能だ。実行結果を以下に示す。

実行結果 実行結果

 なお、上のコードではクラスを使ったイテレータと似たロジックをジェネレータ関数の本体に書いているが、これは次のようにも書ける。

def countup_geniter(limit=5):
    counter = 0
    while True:
        if counter >= limit:
            break
        yield counter
        counter += 1

カウントアップするイテレータをジェネレータ関数を使って書き直したもの(修正版)

 ここでは現在値を表す変数counterの初期値を「0」として、その値を1つ増やす行を本体の最下部に移している。このように書けるのは、ジェネレータイテレータでは、yield式で制御が呼び出し側に戻った後も、その内部でコードのどこが実行されているかが管理されていて、次にnext関数(__next__メソッド)が呼び出されて、制御がジェネレータイテレータに戻ったときには、yield式の次の行(ジェネレータ関数定義の本体の最終行にある「counter += 1」)が実行された後に、次のループが始まるようになるからだ。クラスベースのCountUpIteratorの__next__メソッドでは「return self.counter」行の後に「self.counter += 1」と書いても、その行が実行されることはないので、これと全く同じ書き方はできないことに気を付けよう。

yield式の値とsendメソッド

 ところで、先ほど、「yield 値」は「yield式」だと述べた(これは「return 値」が「return文」であるのと対照的だ)。これは、「yield 値」が戻り値を持つからだ(なお、式は文でもあるので、単純に「yield 値」と書いた場合、それはyield文でもある)。

 yield式の値は、ジェネレータイテレータがどのような形で実行を再開するかによる。今までに見てきたnext関数(ジェネレータイテレータの__next__メソッド)から実行が再開された場合、その値はNoneとなる。式なので、その値は代入演算子を使って任意の変数に代入できる。

 実際に見てみよう。以下は最初に書いたsimple_generatorジェネレータ関数を書き直したものだ。

def simple_generator():
    value = yield 1
    print('value of first yield:', value)
    value = yield 2
    print('value of second yield:', value)
    value = yield 3
    print('value of third yield:', value)

yield式の値を変数に代入する

 ここでは「value = yield 値」としてyield式の値を変数valueに代入している。式の値は、ジェネレータイテレータの実行が再開された時点で変数valueへと代入される(よって、最初に変数valueに値が代入されるので、二度目のnext関数呼び出しが行われた時点だ)。yield式の値が変数に代入されるのは、ジェネレータイテレータの実行が再開された後であることに注意しよう。

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

mygeniter = simple_generator()

print(next(mygeniter))

最初に値を返した時点ではまだ変数に値は代入されていない

 このコードを実行すると「yield 1」が実行され、実行の制御が呼び出し側に戻される。この時点では、変数valueには値は代入されていないし、yield式の次のコードも実行されていない。よって、実行結果は次のようになる。

まだ変数valueにはyield式の値が代入されていない まだ変数valueにはyield式の値が代入されていない

 そして、もう一度、以下のコードを実行すると次のように、変数valueの値(None)が表示される。

print(next(mygeniter))

このnext関数呼び出しで、yiled式の値が変数valueに代入される

 これは、実行がジェネレータイテレータに戻り、「yield 1」という式の値(None)が変数valueに代入され、それがprint関数で呼び出され、最後に「yield 2」で値「2」が呼び出し側に返されたということだ。

yield式の値「None」が表示された yield式の値「None」が表示された

 このようにnext関数(あるいはfor文)でジェネレータイテレータを操作している間は、yield式の値は常にNoneとなる。だが、yield式の値は呼び出し側から設定できる。これにはジェネレータイテレータが持つsendメソッドを使用する。sendメソッドは__next__メソッドと同様に、ジェネレータイテレータの実行を再開するのに使える。sendメソッドに何かの値を渡すと、実行がジェネレータイテレータの側に移り、渡した値が直前に実行したyield式の値となるのだ。実際に試してみよう。

print(mygeniter.send('hello'))

sendメソッドで文字列'hello'をジェネレータイテレータに送信

 これを実行すると次のように、送信した文字列'hello'が表示される。

実行結果 実行結果

 これだけ見ても、あまりメリットは感じられないが、呼び出し側がジェネレータイテレータの処理に関与できるということだ。例えば、次のようなジェネレータ関数を定義してみよう。

def hellomsg_geniter():
    namelist = ['kawasaki', 'isshiki', 'endo']
    counter = 0
    length = len(namelist)
    value = None
    while True:
        if value:  # sendメソッドで名前が送信されてきたときの処理
            namelist.append(str(value))
            length += 1
            counter = length - 1
        value = yield 'hello ' + namelist[counter]
        counter += 1
        if counter % length == 0# リストの末尾要素を使ったら先頭から使う
            counter = 0

ハローメッセージを永遠に生成し続けるジェネレータ関数

 これは「hello ○○」というメッセージを永遠に生成し続けるジェネレータイテレータを返すジェネレータ関数の定義だ。next関数(__next__メソッド)やsendメソッドで、実行を開始(再開)するたびに内部に持つ名前リストの要素を含んだハローメッセージをyieldする(カウンターで現在値を管理して、リスト末尾の要素をyieldしたら、先頭の要素に戻るようにしている)。

 このとき、sendメソッドで名前をジェネレータイテレータに送信すると、それが内部で使用している名前リストに追加されるとともに、それがyieldされ、その次には名前リストの先頭の要素が使われる。

 以下に、使用例を示す。

mygeniter = hellomsg_geniter()
print(next(mygeniter))
print(next(mygeniter))
print(next(mygeniter))
print(next(mygeniter))
print(mygeniter.send('deep insider'))
print(next(mygeniter))

hellomsg_geniterジェネレータ関数の使用例

 このコードは、ジェネレータ関数を呼び出してジェネレータイテレータを作成した後に、next関数でその値をまず4回取り出している。これで名前リストの要素が循環的に使用されているのが確認できるはずだ。次に、sendメソッドでジェネレータイテレータに文字列'deep insider'を送信している。これにより、その名前がリストに追加されて、yieldされた後に(そうするためにカウンターの値も操作している)、名前リストの先頭からまた名前が使用されているのが分かるはずだ。

実行結果 実行結果

 このように、呼び出す側からジェネレータイテレータの処理に関与する場合にはsendメソッドを使用する。なお、ジェネレータイテレータのコードの実行を開始するには、通常、next関数(あるいはfor文内部での同様な機構)が使われるが、最初からsendメソッドを使うことも可能だ。ただし、このときには引数にNoneを指定する必要がある(そうしないと、TypeError例外が発生する)。

 ここまでに見てきた__next__メソッドとsendメソッドに加えて、ジェネレータイテレータ(generatorクラス)にはこの他にも幾つかのメソッドがあるが、これらについては次回に取り上げる。

まとめ

 今回はPythonのジェネレータ関数とそれが生成するジェネレータイテレータの基礎を取り上げた。次回はジェネレータイテレータが持つ他のメソッドや、yield from式、ジェネレータ式と呼ばれる記法について見ていこう。

今回のまとめ

  • イテレータは関数定義の形を使用して、作成することもできる
  • イテレータを作成する関数のことを「ジェネレータ関数」と呼び、ジェネレータ関数の戻り値となるイテレータのことを「ジェネレータイテレータ」と呼ぶ
  • ジェネレータイテレータのことを単純に「ジェネレータ」と呼ぶこともある
  • ジェネレータ関数を記述する際に、呼び出し側に値を戻すときにはreturn文ではなく、yiled式を利用する
  • ジェネレータイテレータのコード(ジェネレータ関数の本体)が実行されるのは、外部からnext関数やfor文の中で、その__next__メソッドが呼び出されたとき
  • ジェネレータイテレータのコードが実行開始されると、最初のyield式までのコードが実行され、値を呼び出し側に戻した時点で、その実行は中断されて、呼び出し側のコードが実行される
  • 中断中のジェネレータイテレータの状態(ローカル変数の値、プログラムの実行位置など)は保存される
  • 次にnext関数やsendメソッドが使われると、yield式の直後から実行が再開される
  • sendメソッドを使うことで、ジェネレータイテレータに値を送信できる
  • sendメソッドで送信された値はyield式の値となるので、これを使って、ジェネレータイテレータの振る舞いを変更できる
  • next関数(__next__メソッド)で実行が再開されたときには、yiled式の値はNoneとなる

前のページへ 1|2       

Copyright© Digital Advantage Corp. All Rights Reserved.

スポンサーからのお知らせPR

注目のテーマ

Microsoft & Windows最前線2025
AI for エンジニアリング
ローコード/ノーコード セントラル by @IT - ITエンジニアがビジネスの中心で活躍する組織へ
Cloud Native Central by @IT - スケーラブルな能力を組織に
システム開発ノウハウ 【発注ナビ】PR
あなたにおすすめの記事PR

RSSについて

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

メールマガジン登録

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