Pythonのジェネレーターを理解する


218

私は現在、Pythonクックブックを読んでおり、現在ジェネレータを調べています。頭を丸くするのが難しいです。

私はJavaの出身ですが、Javaに相当するものはありますか?この本は「プロデューサー/コンシューマー」について話していましたが、スレッディングについて考えていると聞きました。

ジェネレータとは何ですか?なぜそれを使用するのですか?本を引用することなく、明らかに(本から直接、きちんとした単純な答えを見つけられない限り)。たぶん、例を挙げれば、寛大に感じているなら!

回答:


402

注:この投稿は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)得関数であるnn + 1nextすべての値が生成されるまで、を呼び出すたびに単一の値が生成されます。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

9
あなたはsendジェネレータにデータを送ることが可能だと述べました。これを行うと、「コルーチン」ができます。前述のコンシューマー/プロデューサーのようなパターンは、Locksを必要としないためデッドロックできないため、コルーチンを使用して実装するのは非常に簡単です。スレッドをbashせずにコルーチンを説明するのは難しいので、コルーチンはスレッドの非常にエレガントな代替手段であると言います。
Jochen Ritzel、2009年

Pythonジェネレーターは、チューリングマシンの機能の面で基本的にですか?
Fiery Phoenix

48

ジェネレータは、実際には、完了する前に(データ)を返す関数ですが、その時点で一時停止し、その時点で関数を再開できます。

>>> 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   

ジェネレーターは無限ループをカプセル化しますが、要求するたびに各回答しか得られないため、これは問題ではありません。


30

まず第一に、ジェネレータという用語はもともとPythonでいくぶん不明確であり、多くの混乱を招いていました。おそらくイテレータイテラブルを意味しますここを参照)。次に、Pythonにはジェネレータ関数(ジェネレータオブジェクトを返す)、ジェネレータオブジェクト(イテレータ)、ジェネレータ式(ジェネレータオブジェクトとして評価される)もあります。

ジェネレータの用語集のエントリによると、公式用語はジェネレータが「ジェネレータ機能」の略であるということです。以前はドキュメントに一貫性のない用語が定義されていましたが、幸いにもこれは修正されています。

正確であり、「ジェネレーター」という用語は、それ以上の指定がない限り回避することをお勧めします。


2
うーん、少なくともPython 2.6の数行のテストによると、あなたは正しいと思います。ジェネレータ式は、ジェネレータではなくイテレータ(「ジェネレータオブジェクト」)を返します。
Craig McQueen

22

ジェネレータは、イテレータを作成するための省略形と考えることができます。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
...

1
現在、JavaにはStreamsがあり、これはジェネレーターにはるかに似ていますが、驚くほどの手間をかけずに次の要素を取得できないようです。
モニカの訴訟に資金を提供

12

同等の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ループが繰り返されるたびに、ジェネレータが実行されます。ジェネレーターが初めて実行される場合は、最初から実行されます。それ以外の場合は、前回生成された時間から続行されます。


2
最後の段落は非常に重要です。ジェネレーター関数の状態は、sthが生成されるたびに「凍結」され、次回呼び出されたときとまったく同じ状態で継続します。
ヨハネスチャラ2009年

Javaには「ジェネレーター式」に相当する構文はありませんが、ジェネレーター(一度取得すると)は本質的に単なるイテレーターです(Javaイテレーターと同じ基本特性)。
overthink

@overthink:まあ、ジェネレーターには、Javaイテレーターではできない他の副作用があります。私の例でのprint "hello"後に置くとx=x+1、 "hello"は100回出力されますが、forループの本体はまだ33回しか実行されません。
ウェーンジー、2009年

@iWerner:Javaでも同じ効果が得られることを確認してください。同等のJavaイテレーターでのnext()の実装では、0から99まで(mygen(100)の例を使用して)検索する必要があるため、必要に応じてSystem.out.println()を毎回実行できます。ただし、next()から返すのは33回だけです。Javaに欠けているのは、非常に便利なイールド構文で、読み取り(および書き込み)が非常に簡単です。
overthink

私はこの1行のdefを読んで覚えておくのが大好きでした。ジェネレーターが初めて実行される場合は、最初から開始されます。
Iqra。

8

私はジェネレーターについて、プログラミング言語とコンピューティングのバックグラウンドが適切なジェネレーターについて、スタックフレームの観点から説明します。

多くの言語では、その上に現在のスタック「フレーム」があるスタックがあります。スタックフレームには、関数に渡される引数を含め、関数にローカルな変数に割り当てられたスペースが含まれます。

関数を呼び出すと、現在の実行ポイント(「プログラムカウンター」または同等のもの)がスタックにプッシュされ、新しいスタックフレームが作成されます。その後、実行は呼び出されている関数の最初に移ります。

通常の関数では、ある時点で関数が値を返し、スタックが「ポップ」されます。関数のスタックフレームは破棄され、実行は前の場所から再開されます。

関数がジェネレーターの場合、値を返すことができます 、yieldステートメントを使用して、スタックフレームを破棄せずます。関数内のローカル変数とプログラムカウンターの値は保持されます。これにより、ジェネレーターを後で再開することができ、yieldステートメントから実行が継続され、より多くのコードを実行して別の値を返すことができます。

Python 2.5より前は、これがすべてのジェネレーターでした。Pythonの2.5は、バック値を渡す機能追加発生にも同様に。そうすることで、渡された値は、ジェネレーターから一時的に制御(および値)を返したyieldステートメントからの式として使用できます。

ジェネレーターの主な利点は、スタックフレームが破棄されるたびにその「状態」がすべて失われる通常の関数とは異なり、関数の「状態」が保持されることです。副次的な利点は、関数呼び出しのオーバーヘッド(スタックフレームの作成と削除)の一部が回避されることですが、これは通常は小さな利点です。


6

Stephan202の回答に追加できる唯一のことは、David BeazleyのPyCon '08プレゼンテーション「Generator Tricks for Systems Programmers」をご覧になることです。どこでも。これは、「Pythonはちょっと面白そう」から「これが私が探していたもの」へと私を導いたものです。それはATのhttp://www.dabeaz.com/generators/


6

これは、関数fooとジェネレーターfoo(n)を明確に区別するのに役立ちます。

def foo(n):
    yield n
    yield n+1

fooは関数です。foo(6)はジェネレーターオブジェクトです。

ジェネレータオブジェクトを使用する一般的な方法は、ループ内にあります。

for n in foo(6):
    print(n)

ループプリント

# 6
# 7

ジェネレータは再開可能な関数と考えてください。

yieldreturn生成された値がジェネレータによって「返される」という意味で動作します。ただし、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エラーをスローします。


5

この投稿では、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では任意の大きな整数を許可しています)。

ソース


3

イテレータとジェネレータが最初に登場したのは、約20年前のIconプログラミング言語でした。

アイコンの概要をお楽しみいただけます。これにより、構文に集中することなく頭を覆い隠すことができます(アイコンはおそらく知らない言語であり、グリスウォルドは他の言語から来た人々に彼の言語の利点を説明していたため)。

そこでほんの数段落を読んだ後、ジェネレータとイテレータの有用性がより明らかになるかもしれません。


2

リスト内包表記の経験は、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())

もっと


1

ジェネレーターに関する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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.