ジェネレーターは、Pythonにおける特別なイテラブルオブジェクトで、全コレクションを一度にメモリに占有せずに「オンザフライ」でシーケンスを作成することができます。yieldキーワードを使った関数や、ジェネレーター式((expr for ... in ...))によって実装されます。これは、大量のデータや無限のストリームを扱うときに便利です。
リスト内包表現との主な違い:
[x for x in range(10)])は、メモリにすぐに全リストを作成します。(x for x in range(10)))は、一つずつ要素を生成し、はるかに少ないメモリを消費します。ジェネレーターを使用するタイミング:
# ジェネレーター関数 def counter(n): for i in range(n): yield i for number in counter(5): print(number)
「yieldを使った関数と、リストを返す通常の関数の違いは何ですか?例を挙げてください。」
回答:
通常の関数は即座にリストを計算して返し、全要素のためにメモリを占有します。yieldを使った関数はジェネレーターを返し、一つずつ要素を出力し、全シーケンスをすぐにはメモリにロードしません。
def make_list(n): return [i for i in range(n)] # 即座にリストを返し、多くのメモリを占有します。 def make_generator(n): for i in range(n): yield i # 一つずつ要素を出力します。
物語
大きなログを解析するプロジェクトで、エラーメッセージを含む行を抽出するためにリスト内包表現が使用されました:
error_lines = [line for line in open('biglog.txt') if 'ERROR' in line]
ファイルは2GBを超えており、アプリケーションはOOM(Out of Memory)でクラッシュしました。ジェネレーターを使用する必要がありました:
error_lines = (line for line in open('biglog.txt') if 'ERROR' in line)
物語
従業員は短いリストを分析したいと考え、yieldを使った関数を書きましたが、戻り値がリストではなくジェネレーターであることを忘れていました:
result = my_generator_function() # resultはリストではなくジェネレーターです if len(result) > 5: # TypeError: 'generator'型のオブジェクトにはlen()がありません
修正:結果をlist()でラップします。
物語
ジェネレーターを何度もイテレートしようとしました:
numbers = (i for i in range(5)) for n in numbers: pass # ジェネレーターを使い果たしました for n in numbers: print(n) # 何も出力されません
ジェネレーターは一度きりです。再利用するには新しいものを作成する必要があります。