3.1 ジェネレーターとの出会い
ジェネレーターはiteratorオブジェクトを返す関数のことだよ。これらのiteratorは、リクエストに応じて値を生成するから、大量のデータセットをメモリに完全に読み込まずに処理できるんだ。
ジェネレーターの作り方はいくつかあるけど、ここでは一番ポピュラーなものを紹介するね。
関数を使ったジェネレーター
ジェネレーターは関数内でyield
キーワードを使うことで作成されるよ。yield
を持つ関数が呼び出されると、ジェネレーターオブジェクトを返すけど、その瞬間には関数内のコードは実行されないんだ。それよりもyield
の式で実行が停止して、ジェネレーターオブジェクトの__next__()
メソッドが呼ばれるごとに再開されるんだ。
def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1
counter = count_up_to(5)
print(next(counter)) # 出力: 1
print(next(counter)) # 出力: 2
print(next(counter)) # 出力: 3
print(next(counter)) # 出力: 4
print(next(counter)) # 出力: 5
関数にyield
がある場合、Pythonはその関数を通常の関数の実行の代わりに、実行状態を管理するジェネレーターオブジェクトを作成するんだ。
ジェネレーター式
ジェネレーター式はリスト内包表記に似ているけど、角括弧の代わりに丸括弧を使って作成されるんだ。これらもジェネレーターオブジェクトを返すよ。
squares = (x ** 2 for x in range(10))
print(next(squares)) # 出力: 0
print(next(squares)) # 出力: 1
print(next(squares)) # 出力: 4
どの方法が好き?
3.2 ジェネレーターの利点
メモリの効率的な使用
ジェネレーターはオンデマンドで値を計算するから、大量のデータをメモリに完全に読み込まずに処理できるよ。これにより、ジェネレーターは大規模なデータセットやデータストリームを扱うのに最適な選択肢となるんだ。
def large_range(n):
for i in range(n):
yield i
for value in large_range(1000000):
# 一度に一つの値を処理
print(value)
遅延評価
ジェネレーターは遅延評価を行うので、必要なときにのみ値を計算するんだ。これにより、不必要な計算を避け、パフォーマンスが向上するんだ。
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
便利な構文
ジェネレーターはiteratorを作成するための便利な構文を提供していて、コードを書くのも読むのも簡単になるよ。
3.3 ジェネレーターの使用
標準ライブラリでのジェネレーターの使用例
Pythonの標準ライブラリでは、多くの関数がジェネレーターを使用しているよ。例えば、関数range()
は、数のシーケンスを生成するジェネレーターオブジェクトを返すんだ。
for i in range(10):
print(i)
うん、世界はもう変わったよね。
無限シーケンスの作成
ジェネレーターを使うと、無限に続くシーケンスを作成できるんだ。これは、無限のデータストリームを生成する時など、様々なシナリオで役立つよ。
def natural_numbers():
n = 1
while True:
yield n
n += 1
naturals = natural_numbers()
for _ in range(10):
print(next(naturals))
send()
とclose()
の使用
ジェネレーターオブジェクトはsend()
とclose()
メソッドをサポートしていて、これらを使って値をジェネレーターに送り返したり、実行を終了させることができるんだ。
def echo():
while True:
received = yield
print(received)
e = echo()
next(e) # ジェネレーターを始動させる
e.send("Hello, world!") # 出力: Hello, world!
e.close()
3.4 実践でのジェネレーター
ジェネレーターと例外
ジェネレーターは例外処理もできるから、より堅牢なコードを書くための強力なツールになるよ。
def controlled_execution():
try:
yield "Start"
yield "Working"
except GeneratorExit:
print("ジェネレーターが閉じられた")
gen = controlled_execution()
print(next(gen)) # 出力: Start
print(next(gen)) # 出力: Working
gen.close() # 出力: ジェネレーターが閉じられた
例外の扱いについては今後のレクチャーで見ていくけど、ジェネレーターがうまくそれらを扱えることを知っておくと役に立つよ。
入れ子のジェネレーター
ジェネレーターは入れ子にできるから、複雑なイテレーション構造を作成することができるよ。
def generator1():
yield from range(3)
yield from "ABC"
for value in generator1():
print(value)
# 出力
0
1
2
A
B
C
説明:
yield from
: この構造は別のジェネレーターに動作を委譲するために使われ、コードを簡素化し、可読性を向上させるよ。
ジェネレーターとパフォーマンス
ジェネレーターを使うことで、メモリの使用を減らし、イテレーションの実行をより効率的にすることでプログラムのパフォーマンスを大幅に向上させることができるよ。
リストとジェネレーターの比較例
import time
import sys
def memory_usage(obj):
return sys.getsizeof(obj)
n = 10_000_000
# リストの使用
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)
# ジェネレーターの使用
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp) # 結果の比較のために合計を計算
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)
print(f"リスト:")
print(f" 時間: {list_time:.2f} 秒")
print(f" メモリ: {list_memory:,} バイト")
print(f"\nジェネレーター:")
print(f" 時間: {gen_time:.2f} 秒")
print(f" メモリ: {gen_memory:,} バイト")
リスト:
時間: 0.62 秒
メモリ: 89,095,160 バイト
ジェネレーター:
時間: 1.13 秒
メモリ: 200 バイト
GO TO FULL VERSION