Python ジェネレーター関数は何に使用できますか?
質問
私は Python を学び始めており、ジェネレーター関数、つまりその中に yield ステートメントを含む関数に出会いました。これらの関数が実際にどのような種類の問題の解決に適しているのかを知りたいです。
解決
ジェネレーターは遅延評価を提供します。これらを使用するには、'for' を使用して明示的に反復するか、反復する関数または構造に暗黙的に渡します。ジェネレーターはリストを返すかのように複数の項目を返すものと考えることができますが、一度にすべてを返すのではなく 1 つずつ返し、ジェネレーター関数は次の項目が要求されるまで一時停止されます。
ジェネレーターは、すべての結果が必要かどうかわからない場合や、同時にすべての結果にメモリを割り当てたくない場合に、大規模な結果セットを計算する場合 (特にループ自体に関係する計算) に適しています。 。または、ジェネレーターが使用する状況の場合 別の ジェネレーターを使用したり、他のリソースを消費したりするため、それができるだけ遅い方が便利です。
ジェネレーターのもう 1 つの用途 (実際には同じです) は、コールバックを反復に置き換えることです。状況によっては、関数で多くの作業を実行し、呼び出し元に時々レポートを返したい場合があります。従来は、これにはコールバック関数を使用していました。このコールバックを作業関数に渡すと、このコールバックが定期的に呼び出されます。ジェネレーターのアプローチでは、作業関数 (現在はジェネレーター) はコールバックについて何も知らず、何かを報告したいときに単に降伏するだけです。呼び出し元は、別個のコールバックを作成してそれを作業関数に渡す代わりに、ジェネレーターの周りの小さな 'for' ループですべてのレポート作業を実行します。
たとえば、「ファイルシステム検索」プログラムを作成したとします。検索を全体的に実行し、結果を収集して、一度に 1 つずつ表示することができます。最初の結果を表示する前に、すべての結果を収集する必要があり、すべての結果が同時にメモリ内に保存されることになります。あるいは、結果を見つけながら結果を表示することもできます。これにより、メモリ効率が向上し、ユーザーにとってよりフレンドリーになります。後者は、結果出力関数をファイルシステム検索関数に渡すことによって実行することも、検索関数をジェネレーターにして結果を反復処理するだけによって実行することもできます。
後者の 2 つのアプローチの例を見たい場合は、os.path.walk() (コールバックを備えた古いファイルシステムウォーキング関数) と os.walk() (新しいファイルシステムウォーキングジェネレーター) を参照してください。本当にすべての結果をリストに収集したい場合は、ジェネレーター アプローチをビッグリスト アプローチに変換するのは簡単です。
big_list = list(the_generator)
他のヒント
ジェネレーターを使用する理由の 1 つは、ある種のソリューションについてソリューションをより明確にすることです。
もう 1 つは、一度に 1 つずつ結果を処理し、分割して処理する膨大な結果のリストを作成することを避けることです。
次のような fibonacci-up-to-n 関数がある場合:
# function version
def fibon(n):
a = b = 1
result = []
for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
次のように関数をより簡単に書くことができます。
# generator version
def fibon(n):
a = b = 1
for i in xrange(n):
yield a
a, b = b, a + b
機能がより明確になります。そして、この関数を次のように使用すると:
for x in fibon(1000000):
print x,
この例では、ジェネレーター バージョンを使用している場合、1000000 個の項目リスト全体はまったく作成されず、一度に 1 つの値のみが作成されます。リスト バージョンを使用する場合は、リストが最初に作成されるため、これは当てはまりません。
「モチベーション」セクションを参照してください。 PEP255.
ジェネレーターの目立たない使用法は、割り込み可能な関数を作成することです。これにより、スレッドを使用せずに、UI を更新したり、複数のジョブを「同時に」 (実際にはインターリーブで) 実行したりすることができます。
私の疑問を解消するこの説明を見つけました。知らない人がいる可能性もあるので Generators
それも知りません yield
戻る
return ステートメントでは、すべてのローカル変数が破棄され、結果の値が呼び出し元に返されます (返されます)。しばらくして同じ関数が呼び出された場合、関数は新しい変数セットを取得します。
収率
しかし、関数を終了するときにローカル変数が破棄されなかったらどうなるでしょうか?これは、次のことができることを意味します。 resume the function
中断したところ。ここでのコンセプトは、 generators
が紹介されており、 yield
ステートメントは次の場所から再開されます。 function
やめた。
def generate_integers(N):
for i in xrange(N):
yield i
In [1]: gen = generate_integers(3)
In [2]: gen
<generator object at 0x8117f90>
In [3]: gen.next()
0
In [4]: gen.next()
1
In [5]: gen.next()
それが違いです return
そして yield
Python でのステートメント。
Yield ステートメントは、関数をジェネレーター関数にするものです。
したがって、ジェネレーターはイテレーターを作成するためのシンプルかつ強力なツールです。これらは通常の関数のように記述されますが、 yield
データを返したいときはいつでもステートメントを使用します。next() が呼び出されるたびに、ジェネレーターは中断したところから再開します (ジェネレーターはすべてのデータ値と最後に実行されたステートメントを記憶しています)。
現実世界の例
MySQL テーブルに 1 億個のドメインがあり、各ドメインの Alexa ランクを更新したいとします。
最初に必要なことは、データベースからドメイン名を選択することです。
テーブル名が domains
そして列名は domain
.
使用する場合 SELECT domain FROM domains
1億行が返されるため、大量のメモリが消費されます。そのため、サーバーがクラッシュする可能性があります。
そこで、プログラムをバッチで実行することにしました。バッチサイズが 1000 であるとします。
最初のバッチでは、最初の 1000 行をクエリし、各ドメインの Alexa ランクを確認し、データベース行を更新します。
2 番目のバッチでは、次の 1000 行を処理します。3 番目のバッチでは、2001 年から 3000 年までになります。
ここで、バッチを生成するジェネレーター関数が必要になります。
これが私たちのジェネレーター関数です:
def ResultGenerator(cursor, batchsize=1000):
while True:
results = cursor.fetchmany(batchsize)
if not results:
break
for result in results:
yield result
ご覧のとおり、私たちの関数は次のようになります。 yield
結果を出します。キーワードを使用した場合 return
の代わりに yield
, の場合、return に達すると関数全体が終了します。
return - returns only once
yield - returns multiple times
関数がキーワードを使用する場合 yield
それなら発電機だ。
これで、次のように繰り返すことができます。
db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
doSomethingWith(result)
db.close()
バッファリング。データを大きなチャンクでフェッチし、それを小さなチャンクで処理するのが効率的である場合、ジェネレーターが役立つ場合があります。
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
上記により、バッファリングと処理を簡単に分離できます。コンシューマ関数は、バッファリングを気にせずに値を 1 つずつ取得できるようになりました。
ジェネレーターは、コードをクリーンアップし、コードをカプセル化してモジュール化する非常にユニークな方法を提供するので、非常に役立ちます。独自の内部処理に基づいて常に値を吐き出すものが必要で、コード内のどこからでも (たとえばループやブロック内だけでなく) 何かを呼び出す必要がある場合、ジェネレーターは次のようになります。 の 使用する機能。
抽象的な例としては、ループ内に存在せず、どこからでも呼び出されると常にシーケンス内の次の数値を返すフィボナッチ数ジェネレーターが挙げられます。
def fib():
first = 0
second = 1
yield first
yield second
while 1:
next = first + second
yield next
first = second
second = next
fibgen1 = fib()
fibgen2 = fib()
これで、コードのどこからでも呼び出すことができる 2 つのフィボナッチ数ジェネレーター オブジェクトができました。これらのオブジェクトは、次のように常に大きなフィボナッチ数を順番に返します。
>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5
ジェネレーターの素晴らしい点は、オブジェクトを作成する手間を省いて状態をカプセル化できることです。それらについての 1 つの考え方は、それらの内部状態を記憶する「関数」として考えることです。
フィボナッチの例はから入手しました Python ジェネレーター - それは何ですか? 少し想像力を働かせれば、発電機が優れた代替手段となる他の多くの状況を思いつくことができます。 for
ループやその他の従来の反復構造。
簡単な説明:を考えてみましょう for
声明
for item in iterable:
do_stuff()
多くの場合、すべてのアイテムが iterable
最初から存在する必要はありませんが、必要に応じてその場で生成できます。これは両方の点ではるかに効率的です
- スペース(すべてのアイテムを同時に保管する必要はありません)と
- (すべての項目が必要になる前に反復が終了する可能性があります)。
また、事前にすべての項目がわからない場合もあります。例えば:
for command in user_input():
do_stuff_with(command)
ユーザーのすべてのコマンドを事前に知る方法はありませんが、コマンドを渡すジェネレーターがある場合は、次のような優れたループを使用できます。
def user_input():
while True:
wait_for_command()
cmd = get_command()
yield cmd
ジェネレーターを使用すると、無限シーケンスを反復することもできますが、コンテナーを反復する場合はもちろん不可能です。
私のお気に入りの使用法は、「フィルター」操作と「リデュース」操作です。
ファイルを読んでいて、「##」で始まる行だけが必要だとします。
def filter2sharps( aSequence ):
for l in aSequence:
if l.startswith("##"):
yield l
その後、適切なループでジェネレーター関数を使用できるようになります。
source= file( ... )
for line in filter2sharps( source.readlines() ):
print line
source.close()
reduce の例も同様です。のブロックを見つける必要があるファイルがあるとします。 <Location>...</Location>
線。[HTML タグではなく、たまたまタグのように見える行。]
def reduceLocation( aSequence ):
keep= False
block= None
for line in aSequence:
if line.startswith("</Location"):
block.append( line )
yield block
block= None
keep= False
elif line.startsWith("<Location"):
block= [ line ]
keep= True
elif keep:
block.append( line )
else:
pass
if block is not None:
yield block # A partial block, icky
繰り返しますが、このジェネレーターを適切な for ループで使用できます。
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
その考え方は、ジェネレーター関数を使用すると、シーケンスをフィルターまたは削減して、一度に 1 つの値ずつ別のシーケンスを生成できるということです。
ジェネレーターを使用できる実際の例は、何らかの形状があり、そのコーナー、エッジなどを反復処理する場合です。私自身のプロジェクト (ソースコード) ここ)私は長方形を持っていました:
class Rect():
def __init__(self, x, y, width, height):
self.l_top = (x, y)
self.r_top = (x+width, y)
self.r_bot = (x+width, y+height)
self.l_bot = (x, y+height)
def __iter__(self):
yield self.l_top
yield self.r_top
yield self.r_bot
yield self.l_bot
これで、長方形を作成し、その角をループできるようになりました。
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
の代わりに __iter__
方法があるかもしれません iter_corners
そしてそれを呼び出します for corner in myrect.iter_corners()
. 。よりエレガントに使えます __iter__
それ以来、クラスインスタンス名を直接使用できるようになりました。 for
表現。
ここにはいくつかの良い答えがありますが、Python を完全に読むこともお勧めします。 関数型プログラミングのチュートリアル これは、ジェネレーターのより強力な使用例のいくつかを説明するのに役立ちます。
- 特に興味深いのは、次のことが可能になったことです。 ジェネレータ関数の外部から yield 変数を更新します, したがって、比較的少ない労力で動的で織り交ぜられたコルーチンを作成することが可能になります。
- こちらもご覧ください PEP 342:強化されたジェネレーターによるコルーチン 詳細については。
Web サーバーがプロキシとして機能するときにジェネレーターを使用します。
- クライアントはサーバーにプロキシされた URL をリクエストします
- サーバーはターゲット URL のロードを開始します
- サーバーは結果を取得するとすぐにクライアントに結果を返します。
ジェネレーターの send メソッドについては触れられていないので、例を示します。
def test():
for i in xrange(5):
val = yield
print(val)
t = test()
# Proceed to 'yield' statement
next(t)
# Send value to yield
t.send(1)
t.send('2')
t.send([3])
これは、実行中のジェネレーターに値を送信できることを示します。以下のビデオのジェネレーターに関するより高度なコース (含む) yield
説明、並列処理用のジェネレーター、再帰制限のエスケープなどから)
ものの山。一連の項目を生成したいが、それらすべてを一度にリストに「実体化」する必要がないときはいつでも。たとえば、素数を返す単純なジェネレーターを作成できます。
def primes():
primes_found = set()
primes_found.add(2)
yield 2
for i in itertools.count(1):
candidate = i * 2 + 1
if not all(candidate % prime for prime in primes_found):
primes_found.add(candidate)
yield candidate
それを使用して、後続の素数の積を生成できます。
def prime_products():
primeiter = primes()
prev = primeiter.next()
for prime in primeiter:
yield prime * prev
prev = prime
これらはかなり単純な例ですが、事前にデータセットを生成せずに大規模 (潜在的には無限!) データセットを処理する場合にこれがどのように役立つかがわかります。これは最も明らかな用途の 1 つにすぎません。
n までの素数を出力するのにも適しています。
def genprime(n=10):
for num in range(3, n+1):
for factor in range(2, num):
if num%factor == 0:
break
else:
yield(num)
for prime_num in genprime(100):
print(prime_num)