イテレーションのプロトコルとジェネレータを使いこなし、データのストリームを遅延評価で、メモリを一定に保ったまま処理しましょう。
このレッスンを Kodokon で開くPythonのforは、ひとつのプロトコルにすぎません。iter(obj)がイテレータを要求し、次にStopIteration例外が出るまでnext()が呼ばれます。イテラブル(リストのように、そのたびに新しいイテレータを渡せるもの)とイテレータ(一度しかたどれないカーソル)をきちんと区別しましょう。このプロトコルを理解することは、なぜあるオブジェクトが「消費されてしまう」のかを理解することです。
numbers = [10, 20, 30]
iterator = iter(numbers)
print(next(iterator))
print(next(iterator))
print(next(iterator))
# next(iterator) would raise StopIterationyieldを含む関数は、呼び出しても実行されません。ジェネレータ、つまり本体が必要に応じて実行されるイテレータを返します。next()のたびに次のyieldまで進み、そこで実行がローカルな状態もろとも中断されます。その直接の帰結として、ストリームの大きさがどうであれメモリは一定のままです。完全なリストを作り上げるかわりに、値をひとつずつ生み出すからです。
def squares(limit):
n = 0
while n < limit:
yield n * n
n += 1
gen = squares(4)
print(next(gen)) # 0
print(list(gen)) # [1, 4, 9]実務での主な利点はパイプラインです。各段階は前の段階のストリームを消費し、自分のストリームを遅延評価で生み出します。最後に消費される(list()、sum()、for)まで、何も実行されません。心に留めておくべきトレードオフがあります。リストは何度もたどれるしスライスもできますが、ジェネレータにはできません。その一方で、ジェネレータは10 GBのファイルをRAMを飽和させずに処理します。ジェネレータ式、つまり(f(x) for x in xs if cond)は、関数を定義せずに単純な段階をまかなってくれます。
lines = [" 12 ", "x", " 7", "", "30"]
def clean(rows):
for row in rows:
row = row.strip()
if row.isdigit():
yield int(row)
def keep_from(values, floor):
return (v for v in values if v >= floor)
pipeline = keep_from(clean(lines), 10)
print(list(pipeline)) # [12, 30]