遅延評価とイテレータの消費
計算の前後で条件を変えて実行時点を確認し、大きな数列から必要な先頭だけを取得します。さらにenumerateとzipを使い、番号や対応付け、長さの不一致、反復子の再利用を調べます。
説明
遅延処理は、結果が必要になった時点で次の値を計算します。一覧を先に全て計算する方法と比べ、不要な後続処理を避けられる一方、途中の状態変化や入力の消費を考慮する必要があります。
計算の前後で条件を変えて実行時点を確認し、大きな数列から必要な先頭だけを取得します。さらにenumerateとzipを使い、番号や対応付け、長さの不一致、反復子の再利用を調べます。
1即時計算と遅延計算を用意する
同じ数列を10倍する処理を、リスト内包表記とジェネレータ式で用意します。
values = [2, 4, 0, 8]; factor = 10; eager = [n * factor for n in values]全件を今計算する; lazy = (n * factor for n in values)必要時に計算する; print(eager)確定済みの結果eagerはこの時点で全件を計算し、20、40、0、80を保持します。lazyは次の値を要求されるまで乗算を実行しません。
2計算前に条件を変更
factorを100にして、遅延処理から最初の一件を取得します。
factor = 100計算条件を変更; print(next(lazy)変更後に計算, eager[0]変更前に計算済みの値)lazyは現在のfactorを読み、200を返します。eagerの最初の値は計算済みの20です。遅延処理を作った時点の外部変数が、常に値ごと保存されるわけではありません。
3残りを取得した後の再利用
lazyの残りを一覧にし、同じ反復子をもう一度一覧にします。
print(list(lazy)残りを全て消費); print(list(lazy)同じ反復子を再使用); print(values)元データは残る残りは400、0、800です。その後は空の一覧になります。元のvaluesは残っていても、lazyの位置は終端です。再計算には新しいジェネレータを作ります。
4必要になった個数を記録する
100万個の数を作れる関数を定義し、実際に生成した個数を記録します。
def numbers(limit, stats):
for value in range(limit)生成可能な範囲:
stats["produced"] += 1生成した回数を記録
yield value一件だけ返して停止statsの更新は、各値を要求してyieldへ到達したときだけ起きます。このカウンタで処理量を確認します。メモリの実測値ではありません。
5先頭だけを取り出す
isliceで先頭3件だけ取得します。
from itertools import islice; stats = {"produced": 0}; stream = numbers(1_000_000, stats)100万件を生成できる反復子; first = list(islice(stream, 3))必要な3件だけ取得; print(first, stats); print(next(stream)その続きの一件, stats)最初は0、1、2の3件だけが生成されます。次のnextで3が生成され、回数は4になります。全件の結果を一覧として保持していませんが、入力として大きなリストを別途持つ場合や、結果を蓄積する場合は、その保存領域も必要です。
6抽出前と抽出後の番号
個数0のpenを除外し、enumerateをどこに置くか比較します。
rows = [("pen", 0), ("book", 2), ("note", 1)]; print(list(enumerate((row for row in rows if row[1] > 0), start=1)抽出後に1から番号付け)); print([(i, row) for i, row in enumerate(rows, start=1)元の一覧に番号付け if row[1] > 0番号付け後に抽出])抽出後に番号を付けるとbookは1、noteは2です。元の一覧へ先に番号を付けるとbookは2、noteは3になります。enumerateの番号は、その地点で読み出す順序に付けるカウンタです。
7zipは位置で組にする
商品名3件と個数2件を組にします。長さを厳密に確認するため、別の反復子も用意します。
names = ["pen", "book", "note"]; quantities = [2, 5]; print(list(zip(names, quantities)同じ順番の要素を組にする)); left = iter(names); right = iter(quantities)厳密検査用の反復子penと2、bookと5の組ができます。通常のzipは短い方の終端で止まるため、noteは結果に入りません。商品名などのキーで照合しているわけではなく、順番で対応付けています。
8長さの不一致を検出する
strict=Trueで長さが一致しない場合をエラーにします。エラー後に入力がどこまで消費されたかも表示します。
try:
print(list(zip(left, right, strict=True)長さの不一致を拒否))
except ValueError as error不一致を確認:
print(type(error).__name__, str(error))
print(list(left), list(right)失敗後に残った入力)三組目を作る段階で、右側が先に終わったためValueErrorになります。左側のnoteはその確認時にすでに読み出されており、残りは両方とも空です。失敗した操作が入力の位置まで元に戻すとは限りません。
9集計でも反復子は消費される
同じ反復子をsumとlistへ渡します。
amounts = iter([120, 200, 80]); print(sum(amounts)反復子を最後まで消費); print(list(amounts)消費済みの残り); stored = [120, 200, 80]再利用する値を保存; print(sum(stored), list(stored)一覧から別々に読み出す)反復子はsumで全て消費され、後からlistへ渡すと空です。一覧storedなら、sumが独立した反復子を使うため、元の一覧から再び読み出せます。繰り返し必要な結果を保存するか、入力を再生成するかを設計します。
まとめ
遅延処理は値を要求した時点で計算します。外部状態の変化や、計算途中の失敗が影響する時点を確認します。
必要な分だけ生成すれば未使用部分の計算や全結果の保存を避けられます。入力や蓄積結果の保存量は別に考えます。
enumerateは順序へ番号を付け、zipは位置で組にします。これらや集計に渡した反復子の進行位置は戻りません。
操作履歴と解説を振り返れます。
Python 3
values = [2, 4, 0, 8]; factor = 10; eager = [n * factor for n in values]; lazy = (n * factor for n in values); print(eager)[20, 40, 0, 80]
factor = 100; print(next(lazy), eager[0])200 20
print(list(lazy)); print(list(lazy)); print(values)[400, 0, 800] [] [2, 4, 0, 8]
def numbers(limit, stats):
for value in range(limit):
stats["produced"] += 1
yield valuefrom itertools import islice; stats = {"produced": 0}; stream = numbers(1_000_000, stats); first = list(islice(stream, 3)); print(first, stats); print(next(stream), stats)[0, 1, 2] {'produced': 3}
3 {'produced': 4}rows = [("pen", 0), ("book", 2), ("note", 1)]; print(list(enumerate((row for row in rows if row[1] > 0), start=1))); print([(i, row) for i, row in enumerate(rows, start=1) if row[1] > 0])[(1, ('book', 2)), (2, ('note', 1))]
[(2, ('book', 2)), (3, ('note', 1))]names = ["pen", "book", "note"]; quantities = [2, 5]; print(list(zip(names, quantities))); left = iter(names); right = iter(quantities)[('pen', 2), ('book', 5)]try:
print(list(zip(left, right, strict=True)))
except ValueError as error:
print(type(error).__name__, str(error))
print(list(left), list(right))ValueError zip() argument 2 is shorter than argument 1 [] []
amounts = iter([120, 200, 80]); print(sum(amounts)); print(list(amounts)); stored = [120, 200, 80]; print(sum(stored), list(stored))400 [] 400 [120, 200, 80]
演習完了