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


回答:


239

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

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

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

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

後者の2つのアプローチの例を見たい場合は、os.path.walk()(コールバックを備えた古いファイルシステムウォーキング関数)およびos.walk()(新しいファイルシステムウォーキングジェネレーター)を参照してください。もちろん、あなたは本当にすべての結果をリストに集めたかったのですが、ジェネレーターアプローチはビッグリストアプローチに変換するのは簡単です:

big_list = list(the_generator)

ファイルシステムリストを生成するジェネレータなどのジェネレータは、そのジェネレータをループで実行するコードと並行してアクションを実行しますか?理想的には、コンピューターはループの本体を実行し(最後の結果を処理)、同時にジェネレーターが次の値を取得するために実行する必要があることをすべて実行します。
Steven Lu

@StevenLu:次の結果を得るために、前yieldjoin後のスレッドを手動で起動するのが面倒でない限り、並行して実行されません(標準のライブラリジェネレーターはこれを実行しません。密かにスレッドを起動することは避けられます)。ジェネレーターyieldは、次の値が要求されるまで、それぞれ一時停止します。ジェネレーターがI / Oをラップしている場合、OSはファイルからのデータをプロアクティブにキャッシュする可能性があります。
ShadowRanger 2016

90

ジェネレータを使用する理由の1つは、ある種のソリューションに対してソリューションをより明確にすることです。

もう1つは、一度に1つずつ結果を処理することであり、いずれにせよ分離して処理する結果の巨大なリストを構築することを避けます。

次のようなフィボナッチアップ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つの値のみが作成されます。これは、リストが最初に作成されるリストバージョンを使用する場合には当てはまりません。


18
そして、リストが必要な場合は、いつでも行うことができますlist(fibon(5))
エンドリス

41

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

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


1
動機のセクションは、特定の例があるという点で優れています。「プロデューサー関数が、生成された値の間で状態を維持する必要があるほど難しい仕事をしている場合、ほとんどのプログラミング言語は、プロデューサーの引数にコールバック関数を追加する以外に快適で効率的なソリューションを提供しません。リスト...たとえば、標準ライブラリのtokenize.pyはこのアプローチを採用しています。 "
Ben Creasy

38

私の疑いを晴らすこの説明を見つけます。知らない人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()

だから違いだreturnyieldPythonで記述が含まれます。

Yieldステートメントは、関数をジェネレーター関数にするものです。

したがって、ジェネレーターはイテレーターを作成するためのシンプルで強力なツールです。これらは通常の関数のように書かれていますが、yieldデータを返したいときにはいつでもステートメントを使用します。next()が呼び出されるたびに、ジェネレーターは中断したところから再開します(すべてのデータ値と最後に実行されたステートメントを記憶しています)。


33

実世界の例

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

収量を再帰的/動的プログラミングの観点から説明できれば、より実用的です!
igaurav 2014年

27

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

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

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


3
getBigChuckOfDataが遅延でない場合、ここで利回りがどのように向上するか理解できません。この関数の使用例は何ですか?
Sean Geoffrey Pietz 2014年

1
しかし、要点は、IIUCでbufferedFetchがgetBigChunkOfDataの呼び出しを遅延させていることです。getBigChunkOfDataがすでに遅延している場合、bufferedFetchは役に立ちません。bufferedFetch()を呼び出すたびに、BigChunkがすでに読み込まれていたとしても、1つのバッファー要素が返されます。yieldの仕組みは暗黙的にそれを行うため、返す次の要素の数を明示的に保持する必要はありません。
hmijailは、2017

21

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

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

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からフィボナッチの例を取得しました-それらは何ですか?少し想像力をかければ、ジェネレーターがforループや他の従来の反復構造の優れた代替手段となる他の多くの状況を思い付くでしょう。


19

簡単な説明: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

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


...そして無限のシーケンスは、小さなリストを繰り返し循環して、最後に到達した後に最初に戻ることによって生成されるものである可能性があります。グラフで色を選択したり、テキストで忙しいドキドキやスピナーを作成したりするためにこれを使用します。
Andrej Panjkov 2012年

@mataap:そのitertoolためのものがあります-を参照してくださいcycles
martineau 2014年

12

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

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

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つの値で別のシーケンスを生成するというものです。


8
fileobj.readlines()ファイル全体をメモリ内のリストに読み込み、ジェネレータを使用する目的を無効にします。ファイルオブジェクトは既に反復可能であるため、for b in your_generator(fileobject):代わりに使用できます。これにより、ファイル全体を読み取らないように、ファイルは一度に1行ずつ読み取られます。
nosklo 2008

reduceLocationはリストを生成するのはかなり奇妙です、なぜ各行だけを生成しないのですか?また、filterとreduceは、予想される動作を備えたビルトインです(ipythonなどのヘルプを参照)。「reduce」の使用法はfilterと同じです。
ジェームズアンティル

readlines()の良い点。私は通常、ユニットテスト中にファイルが一流の行反復子であることを認識しています。
S.Lott、2008

実際、「削減」とは、複数の個別の線を組み合わせて複合オブジェクトにすることです。さて、それはリストですが、それでもソースから取られた削減です。
S.Lott、2008

9

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

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です。


私はジェネレーターとして同様のクラスフィールドを渡すというアイデアを
気に入りました

7

基本的に、入力維持状態を繰り返すときにコールバック関数を回避します。

ジェネレータを使用して実行できる処理の概要については、ここここを参照してください。


4

ここでいくつかの良い答えがありますが、ジェネレーターのより強力なユースケースのいくつかを説明するのに役立つPython 関数型プログラミングチュートリアルを完全に読むこともお勧めします。


3

ジェネレータの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のジェネレーターに関するDavid Beazley


2

私たちのWebサーバーがプロキシとして機能しているときにジェネレータを使用します。

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

1

ものの山。アイテムのシーケンスを生成したいが、一度にすべてを「具体化」してリストにする必要がない場合。たとえば、素数を返す単純なジェネレーターがあるとします。

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つにすぎません。


いずれでもない場合(primes_foundの素数の候補%素数)はすべての場合(primes_foundの素数の候補%素数)
rjmunro

はい、私は「もしそうでなければ(primes_foundの素数の候補%素数== 0)と書くつもりでした。しかし、あなたの方が少しすっきりしています。:)
Nick Johnson

すべてではないにしても「not」を削除するのを忘れたようです(primes_foundの候補の%素数)
Thava

0

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