Python:ジェネレータ式と収量


90

Pythonでは、ジェネレーター式を介してジェネレーターオブジェクトを作成することと、yieldステートメントを使用することの間に違いはありますか?

利回りの使用:

def Generator(x, y):
    for i in xrange(x):
        for j in xrange(y):
            yield(i, j)

ジェネレータ式を使用する

def Generator(x, y):
    return ((i, j) for i in xrange(x) for j in xrange(y))

どちらの関数も、(0,0)、(0,1)などのタプルを生成するジェネレーターオブジェクトを返します。

どちらか一方の利点はありますか?考え?


みんなありがとう!これらの回答には多くの優れた情報と参考文献があります!


2
最も読みやすいと思われるものを選択してください。
user238424 2010年

回答:


74

2つの違いはわずかです。disこのモジュールを使用して、この種のことを自分で調べることができます。

編集:最初のバージョンでは、対話型プロンプトのmodule-scopeで作成されたジェネレータ式を逆コンパイルしました。これは、関数内で使用されるOPのバージョンとは少し異なります。問題の実際のケースと一致するようにこれを変更しました。

以下に示すように、 "yield"ジェネレーター(最初のケース)のセットアップには3つの追加の命令がありますが、最初からのFOR_ITER違いは1つだけです。 "yield"アプローチでは、ループの内部LOAD_FASTにの代わりにを使用LOAD_DEREFします。LOAD_DEREFある「というより遅い」よりもLOAD_FAST、それはわずかに速いの十分大きな値のための生成式より「収率」バージョンを行い、ようにx(外側ループ)ための値がyわずかに速く、各パスにロードされます。値が小さい場合x、セットアップコードのオーバーヘッドが増えるため、少し遅くなります。

ジェネレーター式は通常、そのような関数でラップするのではなく、コード内でインラインで使用されることを指摘する価値もあります。これにより、セットアップのオーバーヘッドが少し取り除かれLOAD_FAST、「yield」バージョンに他の利点があったとしても、小さいループ値に対してジェネレータ式がわずかに速くなります。

どちらの場合も、パフォーマンスの違いはどちらか一方を決定することを正当化するのに十分ではありません。読みやすさははるかに重要なので、目前の状況で最も読みやすいと感じるものを使用します。

>>> def Generator(x, y):
...     for i in xrange(x):
...         for j in xrange(y):
...             yield(i, j)
...
>>> dis.dis(Generator)
  2           0 SETUP_LOOP              54 (to 57)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_FAST                0 (x)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                40 (to 56)
             16 STORE_FAST               2 (i)

  3          19 SETUP_LOOP              31 (to 53)
             22 LOAD_GLOBAL              0 (xrange)
             25 LOAD_FAST                1 (y)
             28 CALL_FUNCTION            1
             31 GET_ITER
        >>   32 FOR_ITER                17 (to 52)
             35 STORE_FAST               3 (j)

  4          38 LOAD_FAST                2 (i)
             41 LOAD_FAST                3 (j)
             44 BUILD_TUPLE              2
             47 YIELD_VALUE
             48 POP_TOP
             49 JUMP_ABSOLUTE           32
        >>   52 POP_BLOCK
        >>   53 JUMP_ABSOLUTE           13
        >>   56 POP_BLOCK
        >>   57 LOAD_CONST               0 (None)
             60 RETURN_VALUE
>>> def Generator_expr(x, y):
...    return ((i, j) for i in xrange(x) for j in xrange(y))
...
>>> dis.dis(Generator_expr.func_code.co_consts[1])
  2           0 SETUP_LOOP              47 (to 50)
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                40 (to 49)
              9 STORE_FAST               1 (i)
             12 SETUP_LOOP              31 (to 46)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_DEREF               0 (y)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                17 (to 45)
             28 STORE_FAST               2 (j)
             31 LOAD_FAST                1 (i)
             34 LOAD_FAST                2 (j)
             37 BUILD_TUPLE              2
             40 YIELD_VALUE
             41 POP_TOP
             42 JUMP_ABSOLUTE           25
        >>   45 POP_BLOCK
        >>   46 JUMP_ABSOLUTE            6
        >>   49 POP_BLOCK
        >>   50 LOAD_CONST               0 (None)
             53 RETURN_VALUE

承認済み-disを使用した違いの詳細な説明。ありがとう!
cschol

LOAD_DEREF「かなり遅い」と主張するソースへのリンクを含めるように更新したので、パフォーマンスが実際に重要である場合は、実際のタイミングtimeitが良いでしょう。理論的な分析はこれまでのところです。
Peter Hansen

36

この例では、そうではありません。しかしyield、より複雑な構成に使用できます。たとえば、呼び出し元からの値も受け入れ、結果としてフローを変更できます。詳細については、PEP 342をお読みください(これは知る価値のある興味深いテクニックです)。

とにかく、最善のアドバイスは、ニーズに合わせてより明確なものを使用することです。

PS以下は、Dave Beazleyによる簡単なコルーチンの例です。

def grep(pattern):
    print "Looking for %s" % pattern
    while True:
        line = (yield)
        if pattern in line:
            print line,

# Example use
if __name__ == '__main__':
    g = grep("python")
    g.next()
    g.send("Yeah, but no, but yeah, but no")
    g.send("A series of tubes")
    g.send("python generators rock!")

8
David Beazleyにリンクするための+1。コルーチンに関する彼のプレゼンテーションは、私が長い間読んだ中で最も感動的なものです。ジェネレータについての彼のプレゼンテーションほど多分、役に立たないかもしれませんが、それでも驚くべきことです。
ロバートロスニー

18

ジェネレータ式に組み込むことができる単純なループの種類に違いはありません。ただし、yieldを使用すると、はるかに複雑な処理を行うジェネレータを作成できます。フィボナッチ数列を生成する簡単な例を次に示します。

>>> def fibgen():
...    a = b = 1
...    while True:
...        yield a
...        a, b = b, a+b

>>> list(itertools.takewhile((lambda x: x<100), fibgen()))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

5
とてもすばらしい+1 ...再帰せずにこんなに短くて甘いfibの実装を見たことがあるとは言えません。
柔道は2010年

紛らわしいほど単純なコードスニペット-フィボナッチはそれを見て幸せになると思います!!
user-asterix

10

使用方法では、ジェネレータオブジェクトとジェネレータ関数の違いに注意してください。

ジェネレーターオブジェクトは、新しいジェネレーターオブジェクトを返すため、再度呼び出すたびに再利用できるジェネレーター関数とは対照的に、1回限りの使用のみです。

ジェネレータ式は実際には、通常、関数でラップせずに「生」で使用され、ジェネレータオブジェクトを返します。

例えば:

def range_10_gen_func():
    x = 0
    while x < 10:
        yield x
        x = x + 1

print(list(range_10_gen_func()))
print(list(range_10_gen_func()))
print(list(range_10_gen_func()))

出力:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

少し異なる使用法と比較してください。

range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))

出力:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

そしてジェネレータ式と比較してください:

range_10_gen_expr = (x for x in range(10))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))

これも出力します:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

8

yieldネストされたループよりも式が複雑な場合は、使用すると便利です。特に、特別な最初または最後の値を返すことができます。検討してください:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)

5

イテレータについて考えるとき、itertoolsモジュールは:

...単独でまたは組み合わせて役立つ、高速でメモリ効率の高いツールのコアセットを標準化します。これらが一緒になって「イテレーター代数」を形成し、純粋なPythonで簡潔かつ効率的に専用ツールを構築できるようにします。

パフォーマンスについては、 itertools.product(*iterables[, repeat])

入力イテラブルのデカルト積。

ジェネレータ式のネストされたforループに相当します。たとえば、product(A, B)はと同じを返します((x,y) for x in A for y in B)

>>> import itertools
>>> def gen(x,y):
...     return itertools.product(xrange(x),xrange(y))
... 
>>> [t for t in gen(3,2)]
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
>>> 

4

はい、違いがあります。

ジェネレータ式の場合(x for var in expr)iter(expr)式がされたときに呼び出される作成

defおよびyieldを使用してジェネレータを作成する場合、次のようになります。

def my_generator():
    for var in expr:
        yield x

g = my_generator()

iter(expr)まだ呼ばれていません。反復時にのみg呼び出されます(まったく呼び出されない場合もあります)。

このイテレータを例にとります:

from __future__ import print_function


class CountDown(object):
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        print("ITER")
        return self

    def __next__(self):
        if self.n == 0:
            raise StopIteration()
        self.n -= 1
        return self.n

    next = __next__  # for python2

このコード:

g1 = (i ** 2 for i in CountDown(3))  # immediately prints "ITER"
print("Go!")
for x in g1:
    print(x)

その間:

def my_generator():
    for i in CountDown(3):
        yield i ** 2


g2 = my_generator()
print("Go!")
for x in g2:  # "ITER" is only printed here
    print(x)

ほとんどのイテレータはで多くのことを行わないため、__iter__この動作を見逃しがちです。現実世界の例では、DjangoのだろうQuerySet、その中のデータをフェッチ__iter__してdata = (f(x) for x in qs)いる間、多くの時間がかかる場合がありますdef g(): for x in qs: yield f(x)続いdata=g()すぐに戻ります。

詳細と正式な定義については、PEP 289-Generator Expressionsを参照してください。


0

まだ指摘されていない一部のコンテキストでは重要になる可能性のある違いがあります。を使用yieldすると、暗黙的にStopIteration(およびコルーチン関連のもの)を発生させるreturn以外の目的で使用できなくなります。

これは、このコードの形式が正しくないことを意味します(そしてそれをインタープリターに渡すと、が得られますAttributeError)。

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']:
            yield item

print(mary_poppins_purse(True).temperature)

一方、このコードは魅力のように機能します。

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        return (item for item in ['lamp', 'mirror', 'coat rack',
                                  'tape measure', 'ficus'])

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