Pythonでのジェネレーター
Pythonにおけるジェネレーターは、リストやタプルのような反復可能な特別なタイプですが、重要な違いがあります。それは、一度にすべての値をメモリに格納するのではなく、イテレートするたびに値を逐次生成することです。これにより、特に大規模なデータセットを扱う場合や、無限の値のシーケンスを生成する場合に、ジェネレーターをメモリ効率の良いものにしています。
ジェネレーターは、関数またはジェネレーター式を使って定義され、ひとつずつ値を出力するために yield
キーワードを使います。ジェネレーター関数で yield
文に遭遇すると、関数の実行は一時的に中断され、値が呼び出し元に返されます。関数の状態は保存されるため、次に呼び出された時に前回終了したところから再開できます。
こちらがジェネレーター関数のシンプルな例です:
def countdown(n):
while n > 0:
yield n
n -= 1
# ジェネレーター関数の使用
for i in countdown(5):
print(i)
# 結果
# 5
# 4
# 3
# 2
# 1
この例では、countdown
関数は n から 1 までのカウントダウンを生成します。yield
文に遭遇するたびに、現在の n の値が生成され、関数の状態が保存されます。ジェネレーターを for ループでイテレートすると、ひとつずつ値が生成されます。
ジェネレーター式は、よりコンパクトにジェネレーターを作成する別の方法です。リスト内包表記に似た構文ですが、角括弧ではなく丸括弧で囲まれます:
squares = (x * x for x in range(1, 6))
for square in squares:
print(square)
# 結果
# 1
# 4
# 9
# 16
# 25
ジェネレーター式はジェネレーター関数と同じように動作し、逐次的に値を生成します。
ジェネレーターを使用する利点には、メモリ使用量の低減や、大規模なデータセットを扱う際のパフォーマンスの改善が含まれます。なぜなら、全体のデータセットをメモリにロードすることなく、要素を一つずつ処理できるからです。ジェネレーターは無限のシーケンスを扱う場合や、事前にいくつの値を生成する必要があるかわからない場合にも有用です。
ジェネレーターで生成されたすべての値をイテレートすると、一般的にはそれを使い果たすことになり、再びイテレートしたい場合はジェネレーターを再作成する必要があります。
長さがない
ジェネレーターには長さがありません。これはメモリ節約の一環ですが、そのために len(countdown)
は型エラーを引き起こします。
ループの外で使用する
主にforループ内で使用されるジェネレーターですが、__next__()
関数を使ってループの外でも使用することができます。これは一般的な使用例ではありません。次の要素を取得するためにこの関数を呼び出す度に、関数を呼び出した次の生成される要素を取得します。この関数を可能な回数以上に呼び出すと、StopIteration例外が発生します。
おぼえておくべきこと:
__next__()
を一度以上呼び出すべき場面であれば、forループが必要かもしれません。
最後の要素を取得する
-1インデックスはジェネレーターには使用できません。なぜなら、長さがないからです。しかし、次の一行を使うと、最後にたどり着くまで全てをループします!
def countdown(n):
while n > 0:
print("generating ", n)
yield n
n -= 1
*_, last = countdown(5)
print(last)
# 結果
# generating 5
# generating 4
# generating 3
# generating 2
# generating 1
# 1
まとめ
要約すると、Pythonのジェネレーターは動的なデータを管理するためのメモリ効率の良いツールです。大規模なデータセットや無限のシーケンスに最適で、逐次的に値を生成します。固定長がなく len()
が機能しないため、主にforループ内で使用されます。ループの外で __next__()
を使用することもできますが、使い過ぎるとStopIteration例外を引き起こす可能性があります。Pythonにおいて、ジェネレーターは効率的なデータ処理のための貴重なソリューションです。
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/wagenrace/generators-in-python-c8