web-dev-qa-db-ja.com

Pythonジェネレーター関数は何に使用できますか?

私はPythonを学び始めており、その中にyieldステートメントを含むジェネレーター関数に出会いました。これらの関数が解決に本当に優れている問題の種類を知りたいです。

204
quamrana

ジェネレーターは遅延評価を提供します。明示的に 'for'で反復するか、反復する関数または構造に渡すことで暗黙的に反復して使用します。ジェネレーターは、リストを返すかのように複数のアイテムを返すと考えることができますが、一度にすべてを返すのではなく、次々にアイテムが要求されるまでジェネレーター関数が一時停止します。

ジェネレーターは、すべての結果が必要かどうかわからない場合や、すべての結果に同時にメモリを割り当てたくない場合に、結果の大きなセット(特にループ自体を含む計算)を計算するのに適しています。または、ジェネレーターがanotherジェネレーターを使用するか、他のリソースを消費する状況では、それが可能な限り遅く発生した方が便利です。

ジェネレーターのもう1つの使用法(実際は同じです)は、コールバックを反復で置き換えることです。状況によっては、関数に多くの作業を行わせ、場合によっては呼び出し元にレポートを返す必要があります。従来、これにはコールバック関数を使用していました。このコールバックを仕事関数に渡すと、このコールバックが定期的に呼び出されます。ジェネレーターのアプローチは、仕事関数(現在はジェネレーター)がコールバックについて何も知らず、何かを報告したいときはいつでも譲ることです。呼び出し元は、別個のコールバックを作成してそれを仕事関数に渡す代わりに、ジェネレーターの周りの小さな「for」ループですべてのレポート作業を行います。

たとえば、「ファイルシステム検索」プログラムを作成したとします。検索全体を実行し、結果を収集して、一度に1つずつ表示できます。最初の結果を表示する前に、すべての結果を収集する必要があり、すべての結果が同時にメモリに保存されます。または、検索中に結果を表示することもできます。これにより、メモリ効率が向上し、ユーザーにとって使いやすくなります。後者は、結果出力関数をファイルシステム検索関数に渡すことで実行できます。または、検索関数をジェネレーターにして、結果を反復処理するだけで実行できます。

後者の2つのアプローチの例をご覧になりたい場合は、os.path.walk()(コールバック付きの古いファイルシステムウォーク関数)およびos.walk()(新しいファイルシステムウォークジェネレータ)をご覧ください。もちろん、すべての結果をリストで収集したい場合、ジェネレーターアプローチはビッグリストアプローチに変換するのは簡単です。

big_list = list(the_generator)
227
Thomas Wouters

ジェネレーターを使用する理由の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つの値のみが作成されます。リストが最初に作成されるリストバージョンを使用する場合は、そうではありません。

87
nosklo

PEP 255 の「動機」セクションを参照してください。

ジェネレーターの非自明な使用法は、割り込み可能な関数を作成することです。これにより、UIを更新したり、スレッドを使用せずに複数のジョブを「同時に」(実際にはインターリーブ)実行したりできます。

41
Nickolay

私はこの説明を見つけて、私の疑問をクリアします。 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()が呼び出されるたびに、ジェネレーターは中断した場所から再開します(すべてのデータ値と最後に実行されたステートメントを記憶します)。

37
Mirage

実世界の例

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 - 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()
31
Giri

バッファリング。大きなチャンクでデータをフェッチするのが効率的であるが、小さなチャンクでデータを処理する場合、ジェネレーターが役立ちます。

def bufferedFetch():
  while True:
     buffer = getBigChunkOfData()
     # insert some code to break on 'end of data'
     for i in buffer:    
          yield i

上記により、バッファリングと処理を簡単に分離できます。コンシューマー関数は、バッファリングを心配することなく、値を1つずつ取得できるようになりました。

26
Rafał Dowgird

ジェネレーターは、コードをクリーンアップしたり、コードをカプセル化およびモジュール化する非常にユニークな方法を提供したりするのに非常に役立つことがわかりました。独自の内部処理に基づいて値を絶えず吐き出すために何かが必要な状況で、その何かをコード内のどこからでも呼び出す必要がある場合(たとえば、ループやブロック内だけでなく)、ジェネレーターは使用する機能。

抽象的な例は、ループ内に存在しないフィボナッチ数ジェネレーターであり、どこから呼び出されても常にシーケンスの次の数を返します。

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 Generators-What are they?からフィボナッチの例を入手しましたが、少し想像してみれば、ジェネレーターがforループやその他の従来の反復構造の優れた代替手段となるその他の多くの状況。

21
Andz

簡単な説明:forステートメントを考えます

for item in iterable:
   do_stuff()

多くの場合、iterableのすべてのアイテムは最初からそこにある必要はありませんが、必要に応じてその場で生成できます。これは両方でより効率的です

  • スペース(すべてのアイテムを同時に保存する必要はありません)および
  • 時間(すべてのアイテムが必要になる前に反復が終了する場合があります)。

また、事前にすべてのアイテムを知らない場合もあります。例えば:

for command in user_input():
   do_stuff_with(command)

事前にすべてのユーザーのコマンドを知る方法はありませんが、コマンドを処理するジェネレーターがある場合、次のようなNiceループを使用できます。

def user_input():
    while True:
        wait_for_command()
        cmd = get_command()
        yield cmd

ジェネレーターを使用すると、無限シーケンスを反復することもできます。これは、コンテナーを反復する場合はもちろん不可能です。

18
dF.

私のお気に入りの用途は、「フィルター」操作と「削減」操作です。

ファイルを読んでいて、「##」で始まる行だけが必要だとしましょう。

def filter2sharps( aSequence ):
    for l in aSequence:
        if l.startswith("##"):
            yield l

その後、適切なループでジェネレーター関数を使用できます

source= file( ... )
for line in filter2sharps( source.readlines() ):
    print line
source.close()

リデュースの例は似ています。 <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つの値だけ別のシーケンスを生成できます。

12
S.Lott

ジェネレータを使用できる実用的な例は、何らかの形状があり、そのコーナー、エッジなどを反復処理する場合です。私自身のプロジェクト(ソースコード ここ )には、長方形がありました:

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()で呼び出します。 for式でクラスインスタンス名を直接使用できるので、__iter__を使用する方がよりエレガントです。

8
Pithikos

基本的に、状態を維持する入力を反復処理するときにコールバック関数を回避します。

ジェネレーターを使用してできることの概要については、 here および here を参照してください。

7
MvdD

ここでいくつかの良い答えがありますが、ジェネレーターのより強力なユースケースのいくつかを説明するのに役立つPython Functional Programming tutorial を完全に読むこともお勧めします。

4
songololo

Webサーバーがプロキシとして機能している場合、ジェネレーターを使用します。

  1. クライアントはサーバーにプロキシされたURLを要求します
  2. サーバーはターゲットURLのロードを開始します
  3. サーバーは、結果を取得するとすぐに結果をクライアントに返します。
2
Brian

ジェネレーターの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、並列処理用のジェネレーター、再帰制限の回避など)

PyCon 2014の発電機に関するデビッドビーズリー

2
John Damen

ものの山。アイテムのシーケンスを生成したいが、一度にすべてを「マテリアライズ」してリストにする必要はありません。たとえば、素数を返す単純なジェネレーターを作成できます。

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

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)