回答:
注:この投稿はPython 3.xの構文を前提としています。†
発電機は、単にあなたが呼び出すことができているオブジェクトを返す関数であるnext
ことが提起されるまですべての呼び出しのために、それは、いくつかの値を返すように、StopIteration
すべての値が生成されたことを知らせる、例外を。このようなオブジェクトはイテレータと呼ばれます。
通常の関数return
は、Javaと同様に、を使用して単一の値を返します。ただし、Pythonではと呼ばれる代替手段がありyield
ます。yield
関数内の任意の場所で使用すると、ジェネレーターになります。このコードを確認してください:
>>> def myGen(n):
... yield n
... yield n + 1
...
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
あなたが見ることができるように、myGen(n)
得関数であるn
とn + 1
。next
すべての値が生成されるまで、を呼び出すたびに単一の値が生成されます。for
ループnext
はバックグラウンドで呼び出されるため、次のようになります。
>>> for n in myGen(6):
... print(n)
...
6
7
同様に、ジェネレーター式があります。これは、特定の一般的なタイプのジェネレーターを簡潔に説明する手段を提供します。
>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
ジェネレータ式はリスト内包表記によく似ていることに注意してください。
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
ジェネレーターオブジェクトが1回生成されますが、そのコードは一度にすべて実行されるわけではありません。next
実際にコード(の一部)を実行するための呼び出しのみ。ジェネレーターでのコードの実行は、yield
ステートメントに到達すると停止し、ステートメントに到達すると値が返されます。next
次にthenを呼び出すと、最後のジェネレータの後にジェネレータが残された状態で実行が続行されyield
ます。これは通常の関数との根本的な違いです。これらは常に「先頭」から実行を開始し、値を返すとその状態を破棄します。
この主題についてはまだまだ言わなければならないことがあります。たとえばsend
、ジェネレータにデータを戻すことが可能です(リファレンス)。しかし、これは、ジェネレーターの基本概念を理解するまでは調べないことをお勧めします。
今、あなたは尋ねるかもしれません:なぜジェネレータを使うのですか?いくつかの理由があります。
ジェネレーターは、無限のストリームを記述する自然な方法を可能にします。たとえばフィボナッチ数列を考えてみましょう。
>>> def fib():
... a, b = 0, 1
... while True:
... yield a
... a, b = b, a + b
...
>>> import itertools
>>> list(itertools.islice(fib(), 10))
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
このコードはitertools.islice
、無限ストリームから有限数の要素を取得するために使用します。itertools
モジュール内の関数は、高度なジェネレーターを非常に簡単に作成するための不可欠なツールであるため、よく見ることをお勧めします。
† Pythonについて<= 2.6:上記の例でnext
は__next__
、指定されたオブジェクトのメソッドを呼び出す関数です。Python <= 2.6では、のo.next()
代わりに、少し異なる手法を使用しnext(o)
ます。Python 2.7はnext()
呼び出しを持っている.next
ため、2.7で以下を使用する必要はありません。
>>> g = (n for n in range(3, 5))
>>> g.next()
3
ジェネレータは、実際には、完了する前に(データ)を返す関数ですが、その時点で一時停止し、その時点で関数を再開できます。
>>> def myGenerator():
... yield 'These'
... yield 'words'
... yield 'come'
... yield 'one'
... yield 'at'
... yield 'a'
... yield 'time'
>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words
等々。ジェネレーターの(または1つの)利点は、一度に1つずつデータを処理するため、大量のデータを処理できることです。リストでは、過剰なメモリ要件が問題になる可能性があります。ジェネレータは、リストと同じように反復可能であるため、同じように使用できます。
>>> for word in myGeneratorInstance:
... print word
These
words
come
one
at
a
time
ジェネレータは、無限を処理する別の方法を提供することに注意してください。たとえば、
>>> from time import gmtime, strftime
>>> def myGen():
... while True:
... yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000
ジェネレーターは無限ループをカプセル化しますが、要求するたびに各回答しか得られないため、これは問題ではありません。
まず第一に、ジェネレータという用語はもともとPythonでいくぶん不明確であり、多くの混乱を招いていました。おそらくイテレータとイテラブルを意味します(ここを参照)。次に、Pythonにはジェネレータ関数(ジェネレータオブジェクトを返す)、ジェネレータオブジェクト(イテレータ)、ジェネレータ式(ジェネレータオブジェクトとして評価される)もあります。
ジェネレータの用語集のエントリによると、公式用語はジェネレータが「ジェネレータ機能」の略であるということです。以前はドキュメントに一貫性のない用語が定義されていましたが、幸いにもこれは修正されています。
正確であり、「ジェネレーター」という用語は、それ以上の指定がない限り回避することをお勧めします。
ジェネレータは、イテレータを作成するための省略形と考えることができます。Javaイテレータのように動作します。例:
>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g) # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next() # iterator is at the end; calling next again will throw
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
これがあなたの助けになっていることを願っています。
更新:
他の多くの答えが示しているように、ジェネレーターを作成するにはさまざまな方法があります。上記の例のように括弧構文を使用するか、yieldを使用できます。もう1つの興味深い機能は、ジェネレーターを「無限」にすることができることです。
>>> def infinite_gen():
... n = 0
... while True:
... yield n
... n = n + 1
...
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
Stream
sがあり、これはジェネレーターにはるかに似ていますが、驚くほどの手間をかけずに次の要素を取得できないようです。
同等のJavaはありません。
これは、少し工夫された例です:
#! /usr/bin/python
def mygen(n):
x = 0
while x < n:
x = x + 1
if x % 3 == 0:
yield x
for a in mygen(100):
print a
ジェネレーターには0からnまでのループがあり、ループ変数が3の倍数の場合、変数が生成されます。
for
ループが繰り返されるたびに、ジェネレータが実行されます。ジェネレーターが初めて実行される場合は、最初から実行されます。それ以外の場合は、前回生成された時間から続行されます。
print "hello"
後に置くとx=x+1
、 "hello"は100回出力されますが、forループの本体はまだ33回しか実行されません。
私はジェネレーターについて、プログラミング言語とコンピューティングのバックグラウンドが適切なジェネレーターについて、スタックフレームの観点から説明します。
多くの言語では、その上に現在のスタック「フレーム」があるスタックがあります。スタックフレームには、関数に渡される引数を含め、関数にローカルな変数に割り当てられたスペースが含まれます。
関数を呼び出すと、現在の実行ポイント(「プログラムカウンター」または同等のもの)がスタックにプッシュされ、新しいスタックフレームが作成されます。その後、実行は呼び出されている関数の最初に移ります。
通常の関数では、ある時点で関数が値を返し、スタックが「ポップ」されます。関数のスタックフレームは破棄され、実行は前の場所から再開されます。
関数がジェネレーターの場合、値を返すことができます 、yieldステートメントを使用して、スタックフレームを破棄せずにをます。関数内のローカル変数とプログラムカウンターの値は保持されます。これにより、ジェネレーターを後で再開することができ、yieldステートメントから実行が継続され、より多くのコードを実行して別の値を返すことができます。
Python 2.5より前は、これがすべてのジェネレーターでした。Pythonの2.5は、バック値を渡す機能追加で発生にも同様に。そうすることで、渡された値は、ジェネレーターから一時的に制御(および値)を返したyieldステートメントからの式として使用できます。
ジェネレーターの主な利点は、スタックフレームが破棄されるたびにその「状態」がすべて失われる通常の関数とは異なり、関数の「状態」が保持されることです。副次的な利点は、関数呼び出しのオーバーヘッド(スタックフレームの作成と削除)の一部が回避されることですが、これは通常は小さな利点です。
Stephan202の回答に追加できる唯一のことは、David BeazleyのPyCon '08プレゼンテーション「Generator Tricks for Systems Programmers」をご覧になることです。どこでも。これは、「Pythonはちょっと面白そう」から「これが私が探していたもの」へと私を導いたものです。それはATのhttp://www.dabeaz.com/generators/。
これは、関数fooとジェネレーターfoo(n)を明確に区別するのに役立ちます。
def foo(n):
yield n
yield n+1
fooは関数です。foo(6)はジェネレーターオブジェクトです。
ジェネレータオブジェクトを使用する一般的な方法は、ループ内にあります。
for n in foo(6):
print(n)
ループプリント
# 6
# 7
ジェネレータは再開可能な関数と考えてください。
yield
return
生成された値がジェネレータによって「返される」という意味で動作します。ただし、returnとは異なり、ジェネレーターが次に値を要求されると、ジェネレーターの関数fooは、最後のyieldステートメントの後で、中断したところから再開し、別のyieldステートメントに達するまで実行を続けます。
舞台裏ではbar=foo(6)
、ジェネレータオブジェクトバーを呼び出すと、next
属性を持つように定義されます。
自分で呼び出して、fooから生成された値を取得できます。
next(bar) # Works in Python 2.6 or Python 3.x
bar.next() # Works in Python 2.5+, but is deprecated. Use next() if possible.
fooが終了すると(そして生成された値がなくなると)、呼び出しnext(bar)
はStopInterationエラーをスローします。
この投稿では、Pythonジェネレーターの有用性を説明するためのツールとしてフィボナッチ数を使用します。
この投稿では、C ++コードとPythonコードの両方を取り上げます。
フィボナッチ数列は、0、1、1、2、3、5、8、13、21、34などのシーケンスとして定義されます。
または一般的に:
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
これは非常に簡単にC ++関数に転送できます。
size_t Fib(size_t n)
{
//Fib(0) = 0
if(n == 0)
return 0;
//Fib(1) = 1
if(n == 1)
return 1;
//Fib(N) = Fib(N-2) + Fib(N-1)
return Fib(n-2) + Fib(n-1);
}
ただし、最初の6つのフィボナッチ数を印刷する場合は、上記の関数を使用して多くの値を再計算します。
例:だけFib(3) = Fib(2) + Fib(1)
でなく、Fib(2)
も再計算しFib(1)
ます。計算する値が高ければ高いほど、より悪い結果になります。
したがって、で状態を追跡することにより、上記の内容を書き直したくなるかもしれませんmain
。
// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
int result = pp + p;
pp = p;
p = result;
return result;
}
int main(int argc, char *argv[])
{
size_t pp = 0;
size_t p = 1;
std::cout << "0 " << "1 ";
for(size_t i = 0; i <= 4; ++i)
{
size_t fibI = GetNextFib(pp, p);
std::cout << fibI << " ";
}
return 0;
}
しかし、これは非常に醜く、私たちのロジックを複雑にしますmain
。私たちの状態を心配する必要がない方が良いでしょうmain
関数の。
私たちは返すことができます vector
値のをを使用iterator
してその値のセットを反復することができますが、これには多数の戻り値に対して一度に大量のメモリが必要になります。
さて、以前のアプローチに戻って、数値を印刷する以外に何かしたい場合はどうなりますか?コードのブロック全体をコピーして貼り付ける必要がありますmain
、出力ステートメントを他の目的に変更する必要があります。そして、コードをコピーして貼り付けると、撃たれるはずです。撃たれたくないでしょ?
これらの問題を解決するために、またショットを回避するために、コールバック関数を使用してこのコードブロックを書き直す場合があります。新しいフィボナッチ数が検出されるたびに、コールバック関数を呼び出します。
void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
if(max-- == 0) return;
FoundNewFibCallback(0);
if(max-- == 0) return;
FoundNewFibCallback(1);
size_t pp = 0;
size_t p = 1;
for(;;)
{
if(max-- == 0) return;
int result = pp + p;
pp = p;
p = result;
FoundNewFibCallback(result);
}
}
void foundNewFib(size_t fibI)
{
std::cout << fibI << " ";
}
int main(int argc, char *argv[])
{
GetFibNumbers(6, foundNewFib);
return 0;
}
これは明らかに改善であり、あなたの論理は main
は雑然としていないため、フィボナッチ数列を使用して何でも好きなことができ、新しいコールバックを定義するだけです。
しかし、これはまだ完璧ではありません。最初の2つのフィボナッチ数だけを取得し、次に何かを実行してから、さらにいくつか取得してから、別の何かを実行したい場合はどうしますか?
さて、これまでと同じように続けることができ、状態を再び追加し始めることができます main
続ける、GetFibNumbersを任意のポイントから開始できるようにすることができます。しかし、これはコードをさらに膨らませ、フィボナッチ数を印刷するような単純なタスクにはすでに大きすぎます。
いくつかのスレッドを介して、プロデューサーモデルとコンシューマーモデルを実装できます。しかし、これはコードをさらに複雑にします。
代わりにジェネレータについて話しましょう。
Pythonには、ジェネレータと呼ばれるこれらのような問題を解決する非常に優れた言語機能があります。
ジェネレーターを使用すると、関数を実行し、任意のポイントで停止して、中断したところから再開できます。毎回値を返します。
ジェネレータを使用する次のコードを考えてみます。
def fib():
pp, p = 0, 1
while 1:
yield pp
pp, p = p, pp+p
g = fib()
for i in range(6):
g.next()
結果は次のとおりです。
0 1 1 2 3 5
の yield
ステートメントは、Pythonジェネレーターと組み合わせて使用されます。関数の状態を保存し、yeilded値を返します。次にジェネレーターでnext()関数を呼び出すと、yieldが中断したところから続行されます。
これは、コールバック関数コードよりもはるかにクリーンです。よりクリーンなコード、より小さなコード、そしてはるかに多くの機能的なコードは言うまでもありません(Pythonでは任意の大きな整数を許可しています)。
リスト内包表記の経験は、Python全体に広く使用されていることを示しています。ただし、多くのユースケースでは、完全なリストをメモリ内に作成する必要はありません。代わりに、要素を一度に1つずつ反復するだけで済みます。
たとえば、次の合計コードはメモリ内の正方形の完全なリストを作成し、それらの値を反復処理し、参照が不要になったときにリストを削除します。
sum([x*x for x in range(10)])
代わりにジェネレータ式を使用することにより、メモリが節約されます。
sum(x*x for x in range(10))
コンテナーオブジェクトのコンストラクターにも同様の利点があります。
s = Set(word for line in page for word in line.split())
d = dict( (k, func(k)) for k in keylist)
ジェネレータ式は、反復可能入力を単一の値に削減するsum()、min()、max()などの関数で特に役立ちます。
max(len(line) for line in file if line.strip())
ジェネレーターに関する3つの重要な概念を説明するこのコードを作成しました。
def numbers():
for i in range(10):
yield i
gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers
for i in gen: #we iterate over the generator and the values are printed
print(i)
#the generator is now empty
for i in gen: #so this for block does not print anything
print(i)
send
ジェネレータにデータを送ることが可能だと述べました。これを行うと、「コルーチン」ができます。前述のコンシューマー/プロデューサーのようなパターンは、Lock
sを必要としないためデッドロックできないため、コルーチンを使用して実装するのは非常に簡単です。スレッドをbashせずにコルーチンを説明するのは難しいので、コルーチンはスレッドの非常にエレガントな代替手段であると言います。