私は Python の学習を始めており、yield ステートメントを含むジェネレーター関数に出くわしました。これらの関数が本当に得意とする問題の種類を知りたいです。
16 に答える
ジェネレーターは遅延評価を提供します。「for」を使用して明示的に、または反復する関数または構造に渡すことで暗黙的に、それらを反復して使用します。ジェネレーターは、リストを返すかのように複数のアイテムを返すと考えることができますが、一度にすべてを返すのではなく、1 つずつ返し、ジェネレーター関数は次のアイテムが要求されるまで一時停止します。
ジェネレーターは、すべての結果が必要かどうかわからない場合や、同時にすべての結果にメモリを割り当てたくない場合に、大量の結果セットを計算する場合 (特にループ自体を含む計算) に適しています。 . または、ジェネレーターが別のジェネレーターを使用したり、他のリソースを消費したりする状況では、それができるだけ遅く発生した方が便利です。
ジェネレーターのもう 1 つの用途 (これは実際には同じです) は、コールバックを反復に置き換えることです。場合によっては、関数で多くの作業を行い、時々呼び出し元に報告する必要があります。従来、これにはコールバック関数を使用していました。このコールバックを work-function に渡すと、定期的にこのコールバックが呼び出されます。ジェネレーターのアプローチは、work-function (現在はジェネレーター) がコールバックについて何も知らず、何かを報告したいときはいつでも単に譲歩するというものです。呼び出し元は、別のコールバックを作成してそれを work-function に渡す代わりに、ジェネレーターの周りの小さな「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 つの値だけが作成されます。これは、リストが最初に作成されるリスト バージョンを使用する場合には当てはまりません。
私はこの説明を見つけて、私の疑問を明らかにします。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()
これがPythonのとステートメントreturn
の違いです。yield
Yieldステートメントは、関数をジェネレーター関数にするものです。
したがって、ジェネレーターは、イテレーターを作成するためのシンプルで強力なツールです。これらは通常の関数のように記述されていますが、yield
データを返したいときはいつでもステートメントを使用します。next()が呼び出されるたびに、ジェネレーターは中断したところから再開します(すべてのデータ値と最後に実行されたステートメントを記憶します)。
PEP 255の「動機」セクションを参照してください。
ジェネレーターの非自明な使用法は、割り込み可能な関数を作成することです。これにより、スレッドを使用せずに、UI を更新したり、複数のジョブを「同時に」(実際にはインターリーブされて) 実行したりできます。
実際の例
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
ジェネレーターの優れている点は、オブジェクトを作成するというフープを経ることなく、状態をカプセル化できることです。それらについての一つの考え方は、それらの内部状態を記憶する「関数」としてです。
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 Functional Programming チュートリアルを完全に読むこともお勧めします。
- 特に興味深いのは、ジェネレーター関数の外部から yield 変数を更新できるようになったことです。これにより、比較的少ない労力で動的で織り交ぜられたコルーチンを作成できるようになります。
- 詳細については、PEP 342: 強化されたジェネレーターによるコルーチンも参照してください。
Web サーバーがプロキシとして機能している場合は、ジェネレーターを使用します。
- クライアントはサーバーからプロキシされた URL を要求します
- サーバーはターゲット URL の読み込みを開始します
- サーバーは、結果を取得するとすぐに結果をクライアントに返します。
ものの山。一連のアイテムを生成したいが、それらすべてを一度にリストに「実体化」する必要はありません。たとえば、素数を返す単純なジェネレーターを作成できます。
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 つにすぎません。