問題の歴史
遅延処理(lazy evaluation)は、結果がこのコードで必要になるまで計算を遅らせるプログラミング技法です。Python言語では、このパラダイムはジェネレータやitertoolsのような標準ライブラリの特別な関数のおかげで人気を得ました。このような技術はもともと関数型言語から来ましたが、Pythonは遅延処理のためのネイティブおよびサードパーティのツールを提供しています。
問題
従来のイager(貪欲)処理は、データをすべて一度に読み込んで計算することを要求します(例えば、リスト内包表記を使用する場合)、これは大量のメモリ消費と、大きなシーケンスや無限シーケンスを扱う際のパフォーマンス低下を引き起こす可能性があります。遅延処理は、必要に応じて要素を「読み込み」および処理することを可能にし、不必要なリソースの浪費を避けます。
解決策
Pythonでは、遅延処理はジェネレータ(yield)だけでなく、itertoolsモジュール内の特別な遅延関数、map、filterのような標準関数、以及びジェネレータ式のようなオブジェクトを通じて実現されます。例えば、map()関数は、値を要求されるまで計算しない遅延イテレータを返します:
# 遅延処理の例:各数を二乗する squares = map(lambda x: x ** 2, range(10**10)) # リストのためのメモリは消費しない print(next(squares)) # 0 print(next(squares)) # 1
主な特徴:
Pythonのmap()関数は常に遅延処理を実現していますか?どの標準関数が遅延であるかを知るにはどうすればよいですか?
いいえ、Python 3以降では、map()、filter()、zip()関数はイテレータを返し、つまり遅延処理を実現しています。Python 2では、これらの関数はリストを返していました。オブジェクトが遅延であるかどうかを判断するには、そのタイプを確認するか、ドキュメントを調べる必要があります:
result = map(lambda x: x + 1, range(5)) print(type(result)) # 'map'はイテレータです
sum()関数内でジェネレータ式を使った遅延処理は機能しますか?
sum()関数はイテレータ全体の最後まで通過する必要があります。ジェネレータ式自体は遅延的ですが、sum()は結局すべてのシーケンスを消費します:
s = sum(x**2 for x in range(1000000)) # ジェネレータは完全に消費されます
リストやタプルに対して、例えばmap/lambdaを通じて遅延処理を適用することはできますか?
はい、できますが、リストとタプルは依然としてメモリに読み込まれています。mapはそれらに対する遅延イテレータを返しますが、元のデータは依然としてすべてメモリにあります。完全な遅延チェーンを実現するには、各ステップでジェネレータを使用することが望ましいです:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # すべて遅延的
map(...)を介して)を再度イテレートしようとすることは、予期しないデータの欠如を引き起こしますlist()を通じて)に変換する「早すぎる」ことは、メモリの節約効果を無効にします開発者は、生成器式を使用して行をフィルタするための大きなログファイルの処理を記述しますが、誤ってすぐにリストに変換します:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # ファイル全体を読み込みます
利点:
欠点:
別のチームは、遅延アプローチを使用して生成器式を使用し、受信した行を逐次処理します:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
利点:
欠点: