本当に誰でも読める!! Pythonソースコードの読み方講座(第11回)yield 文とジェネレータ

NTTデータ数理システム MSIISM Conference 2023
  • HOME
  • 本当に誰でも読める!! Pythonソースコードの読み方講座(第11回)yield 文とジェネレータ

本記事は当社が発行しているシミュレーションメールマガジンVol.11の記事です。 シミュレーションメールマガジンの詳細・購読申込はこちら のサポートページから

プログラマにもノン・プログラマにも読んでいただきたい「Pythonコードの読み方」講座の第11回です。今回は yield 文を扱います。yield 文はS4のシミュレーションのコア実装に関わるほか、自身でシミュレーションを記述する際にも登場します。この機会にぜひ理解を深めてください。

ジェネレータの基本

yield が関係するコードの「読み方」としては以下を抑えておけばOKです:

  • 関数定義内に yield 文が含まれている場合、その関数はジェネレータ関数となる
  • ジェネレータオブジェクトを動作させると次の yield 文までの処理が実行され、値が返される

早速例を見ていきましょう。
次のコードでは、yield を含む関数を定義し、呼び出しています:

コード1

def func():
    print("1回目の呼び出し")
    yield 1
    print("2回目の呼び出し")
    yield 2
    print("3回目の呼び出し")
    yield 3
    print("しつこい!")

gen = func()
print(type(func)) 
print(type(gen))

出力1

class 'function'
class 'generator'

yield を含む関数 (ここでは func) のことをジェネレータ関数と呼びます。出力の1行目からも、func が関数であることが確認できます。
ジェネレータ関数を呼び出すと返り値としてジェネレータオブジェクト (ここでは gen) を得ることができます。出力の2行目から、gengenerator のインスタンスであることが確認できます。

ジェネレータオブジェクトは組み込み関数 next を用いることで yield までの処理を逐次的に行うことができます。コード1 に続けて次のコード2 を実行してみます:

コード2

v1 = next(gen)
print(v1)
v2 = next(gen)
print(v2)
v3 = next(gen)
print(v3)

出力2

1回目の呼び出し
1
2回目の呼び出し
2
3回目の呼び出し
3

最初の next の呼び出しで最初の yield までの処理が走ります。このとき print 文が実行されたあとに返り値として yield 11 が得られます。これが3回繰り返された結果、上のような出力が得られます。

この コード2 の実行によって func で定義した処理のすべての yield 文が行われたことになります。さらにもう一度 next を呼び出すとどうなるでしょうか?

コード3

next(gen)

出力3

しつこい!
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
 in 
----> 1 next(gen)

StopIteration: 

怒られたうえにエラーが起きてしまいました。これは next の呼び出しによって以下のような処理が行われたためです:

  • func で定義した処理が最後まで走り、print 文が実行された
  • 関数の終わりに到達し、ジェネレータオブジェクトが返す値が無くなったため、そのことを示すエラーが送出された

ジェネレータからの値の取り出し方

ジェネレータオブジェクトから値を取り出すには next 関数以外にも色々な方法があります。

for 文との組み合わせ

コード4

gen = func()
for item in gen:
    print("item の値は", item)

出力4

1回目の呼び出し
item の値は 1
2回目の呼び出し
item の値は 2
3回目の呼び出し
item の値は 3
しつこい!

コード4 では新たにジェネレータオブジェクトを取得しなおし、これを for 文に与えています。以下のような動作をしています:

  • for 文の各ステップで1つずつ yield の返り値を取得している
  • 最後まで行って StopIteration が返されたら for 文を抜ける

list 関数との組み合わせ

コード5

gen = func()
l = list(gen)
print("リストは", l)
l2 = list(gen)
print("2回目のリストは", l2)

出力5

1回目の呼び出し
2回目の呼び出し
3回目の呼び出し
しつこい!
リストは [1, 2, 3]
2回目のリストは []

コード5 では gen を組み込み型の list に与えることで、gen の生成する値からなるリストを得ています。 for 文のときと同様、 yield が返すすべての要素を順に取得できていることがわかります。
注意点として、一度値を取り出した gen に対してもう一度 list を作用させても値を取得することはできません。 コード5 においても1回目で値を取りきったため2回目の list の実行では空のリストが得られていることがわかります。

ジェネレータの使い所

生成すべき値だちを最初に全て計算できる場合、リスト等のデータ型を用いればよく、ジェネレータを使う必要はありません。 値生成器としてのジェネレータの利用価値は、系列長が無限に長い場合や計算時間やメモリ使用量が大きい場合に出てきます。

無限に値を生成するジェネレータ

コード6

def func(val):
    # 0,1,...val-1 をループして生成しつづける
    while True:
        for v in range(val):
            yield v
gen = func(3)
for _ in range(10):
    print(next(gen))

出力6

0
1
2
0
1
2
0
1
2
0

コード6 では与えた範囲の数字を無限にループして生成するジェネレータ関数を定義し、実際に値を取り出しています。 このように、取り出す値の個数が事前に決まっていなくても動作する値生成器を作るためにはジェネレータを使うか、自作クラスを作る必要があります。

非常に大きいファイルの読み込み

実際に生成しうるすべての値を前もって取得することが困難なケースもあります。
例えば非常に大きいファイルがあり、この中のうち "a" から始まる行だけを抽出したい場合を考えます。ジェネレータを用いることで次のように「ファイルを先頭から読み、"a" で始まる行を見つけたら yield する」プログラムを作ることができます。

コード7

def func():
    with open("large_input.txt", "r") as fp:
        while line := fp.readline():
            if not line:
                break
            if line.startswith("a"):
                yield line
count = 0
for line in func():
    print(line)
    count += 1
    if count==10:
        break

このような処理を行う際にジェネレータを使わずに最初にファイルを全て読み込むプログラムは、計算時間・メモリ使用量の面で効率の悪いものになってしまいます。

まとめ

今回は yield 文を含むコードについて扱いました。 再掲になりますが、コードを読む上では以下のポイントを抑えておけばOKです:

  • 関数定義内に yield 文が含まれている場合、その関数はジェネレータ関数となる
  • ジェネレータオブジェクトを動作させると次の yield 文までの処理が実行され、値が返される

S4のユーザコードの中にも yield 文が現れることがありますが、これらもジェネレータを定義していることになります。yield を区切りとして処理が部分的に少しずつ実行されていくイメージを持つと、コードの理解につながるかもしれません。

次回はジェネレータが持つ、「処理を一時停止したり再開したりできる」性質を利用してより高度なプログラムを作る例を紹介します。

豊岡 祥 株式会社 NTTデータ数理システム シミュレーション&マイニング部所属。
S4 Simulation System の開発のほか、機械学習・シミュレーション・数理最適化を幅広く扱い、分野を横断した問題解決に取り組んでいる。
入社後に競技プログラミングをはじめ、PGBATTLE2023にて企業の部団体3位入賞。

著書: 「XAI(説明可能なAI)──そのとき人工知能はどう考えたのか?」リックテレコム
趣味: 合唱
「数理科学の基礎知識」e-book無料ダウンロードはこちら

関連記事