web-dev-qa-db-ja.com

Pythonのジェネレーターについて

現在Pythonクックブックを読んでおり、現在ジェネレーターを見ています。頭を丸めるのが難しいと感じています。

Javaのバックグラウンドから来たとき、Javaに相当するものはありますか?この本は「プロデューサー/コンシューマー」について話していましたが、聞いたとき、スレッド化について考えています。

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

192
Federer

注:この投稿では、Python 3.x構文を想定しています。

generator は単に、nextを呼び出すことができるオブジェクトを返す関数です。このため、すべての呼び出しでStopIteration例外が発生するまで値を返し、すべての値が生成されたことを通知します。このようなオブジェクトはiteratorと呼ばれます。

通常の関数は、Javaの場合と同様に、returnを使用して単一の値を返します。ただし、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 + 1を生成する関数です。 next を呼び出すたびに、すべての値が生成されるまで単一の値が生成されます。 forループはバックグラウンドでnextを呼び出します。したがって、

>>> for n in myGen(6):
...     print(n)
... 
6
7

同様に generator expression があり、これは特定の一般的なタイプのジェネレータを簡潔に記述する手段を提供します。

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

ジェネレータ式は list comprehensions に非常に似ていることに注意してください:

>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]

ジェネレーターオブジェクトが生成されるonceが、そのコードはnot一度に実行されることに注意してください。 nextの呼び出しのみが、実際にコード(の一部)を実行します。 yieldステートメントに到達すると、ジェネレーターでのコードの実行は停止し、値が返されます。次に、nextを呼び出すと、最後のyieldの後にジェネレーターが残った状態で実行が継続されます。これは、通常の関数との根本的な違いです。関数は常に「先頭」から実行を開始し、値を返すと状態を破棄します。

このテーマについては、さらに多くのことが述べられています。例えばsendデータをジェネレーターに戻すことができます( reference )。しかし、それは、ジェネレーターの基本的な概念を理解するまでは検討しないことをお勧めします。

今、あなたは尋ねるかもしれません:なぜジェネレーターを使うのですか?いくつかの正当な理由があります。

  • 特定の概念は、ジェネレーターを使用してより簡潔に説明できます。
  • 値のリストを返す関数を作成する代わりに、その場で値を生成するジェネレーターを作成できます。これは、リストを作成する必要がないことを意味します。つまり、結果のコードのメモリ効率が高くなります。このようにして、単純に大きすぎてメモリに収まらないデータストリームを記述することさえできます。
  • ジェネレーターは、infiniteストリームを記述する自然な方法を可能にします。たとえば、 フィボナッチ数 を考えてください。

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

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

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

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

47
Caleb Hattingh

まず第一に、generatorという用語はもともとPythonで幾分不明確であり、多くの混乱をもたらしました。あなたはおそらくiteratorsiterablesを意味します( こちらを参照 )。 Pythonには、ジェネレーター関数(ジェネレーターオブジェクトを返す)、generatorもありますobjects(反復子)およびgenerator式(これらは生成オブジェクトに評価されます)。

generator の用語解説によれば、公式用語は今ではgeneratorは「ジェネレーター関数」の略です。過去にドキュメントでは用語の一貫性が定義されていませんでしたが、幸いなことにこれは修正されています。

正確に記述し、詳細を指定せずに「ジェネレータ」という用語を使用しないことをお勧めします。

26
nikow

ジェネレータは、イテレータを作成するための速記と考えることができます。これらは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を使用できます。別の興味深い機能は、ジェネレーターが「無限」である可能性があることです。イテレーターは停止しません。

>>> 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
...
22
overthink

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ループの各反復中に、ジェネレーターが実行されます。ジェネレーターが最初に実行される場合は、最初から開始されます。そうでない場合は、生成された前回から継続します。

11
Wernsey

スタックフレームの観点から、プログラミング言語やコンピューティングの経験が豊富なジェネレーターについて説明します。

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

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

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

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

Python 2.5より前は、これはすべてのジェネレーターが行っていました。 Python 2.5では、ジェネレーターに値を渡すinも追加されました。そうすることで、渡された値は、ジェネレーターから一時的に制御(および値)を返したyieldステートメントの結果の式として利用できます。

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

8
Peter Hansen

関数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エラーがスローされます。

6
unutbu

Stephan202の答えに追加できるのは、David BeazleyのPyCon '08プレゼンテーション「システムプログラマー向けのジェネレータトリック」をご覧になることです。どこでも。これは、「Pythonはちょっと面白そうだ」から「これが私が探していたもの」に私を導いたものです。 http://www.dabeaz.com/generators/ にあります。

6
Robert Rossney

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

ソース

4
Brian R. Bondy

イテレータとジェネレータの最初の登場は、約20年前のIconプログラミング言語にあったと思います。

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

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

2
Nosredna

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

詳細

2
Saqib Mujtaba