Pythonジェネレーターを使用するのに適した時期ではないのはいつですか?


83

これは、Pythonジェネレーター関数を何に使用できるかという逆です。:Pythonジェネレーター、ジェネレーター式、およびitertoolsモジュールは、最近のpythonの私のお気に入りの機能の一部です。これらは、大量のデータに対して実行する一連の操作を設定するときに特に役立ちます。DSVファイルを処理するときによく使用します。

それで、それはいつですか 、ジェネレーター、ジェネレーター式、またはitertools関数を使用するのが適切ないのですか?

  • ときに私が好むはずですzip()以上itertools.izip()、または
  • range()以上xrange()、または
  • [x for x in foo] 以上 (x for x in foo)

明らかに、最終的にはジェネレーターを実際のデータに「解決」する必要があります。通常は、リストを作成するか、ジェネレーター以外のループでリストを反復処理します。長さを知る必要がある場合もあります。これは私が求めているものではありません。

中間データ用に新しいリストをメモリに割り当てないように、ジェネレータを使用します。これは、特に大きなデータセットに適しています。小さなデータセットにも意味がありますか?顕著なメモリ/ CPUのトレードオフはありますか?

リスト内包表記のパフォーマンスとmap()およびfilter()の目を見張るような議論に照らして、誰かがこれについてプロファイリングを行った場合は特に興味があります。(代替リンク


2
私はここ同様の質問を提起し、いくつかの分析を行って、私の特定の例で は、リストが長さの反復可能オブジェクトに対してより高速である<5ことを発見しました
アレクサンダーマクファーレン2016年

これはあなたの質問に答えますか?ジェネレータ式とリスト
内包表記

回答:


57

次の場合は、ジェネレータの代わりにリストを使用します。

1)データに複数回アクセスする必要があります(つまり、結果を再計算する代わりにキャッシュします)。

for i in outer:           # used once, okay to be a generator or return a list
    for j in inner:       # used multiple times, reusing a list is better
         ...

2)ランダムアクセス(または順方向の順序以外のアクセス)が必要です:

for i in reversed(data): ...     # generators aren't reversible

s[i], s[j] = s[j], s[i]          # generators aren't indexable

3)文字列を結合する必要があります(データを2回渡す必要があります)。

s = ''.join(data)                # lists are faster than generators in this use case

4)PyPyを使用していますが、通常の関数呼び出しやリスト操作の場合ほどジェネレーターコードを最適化できない場合があります。


#3の場合ireduce、結合を複製するためにを使用して2つのパスを回避できませんでしたか?
プラチナアズール

ありがとう!ストリングの結合動作を知りませんでした。2つのパスが必要な理由の説明を提供またはリンクできますか?
David Eyk 2014年

5
@DavidEyk str.joinは、すべての文字列フラグメントの長さを合計するために1つのパスを作成するため、結合された最終結果に割り当てる多くのメモリを認識します。2番目のパスは、文字列フラグメントを新しいバッファにコピーして、単一の新しい文字列を作成します。hg.python.org/cpython/file/82fd95c2851b/Objects/stringlib/…を
Raymond Hettinger

1
興味深いことに、私はスリングに参加するためにジェネレーターを頻繁に使用します。しかし、2つのパスが必要な場合、どのように機能するのでしょうか。たとえば''.join('%s' % i for i in xrange(10))
bgusach 2014年

4
@ ikaros45参加する入力がリストでない場合、2つのパスの一時リストを作成するために追加の作業を行う必要があります。大まかに言って、この `` data = data if isinstance(data、list)else list(data); n = sum(map(len、data)); buffer = bytearray(n); ... <フラグメントをバッファにコピー> `` `。
Raymond Hettinger 2014年

40

一般に、len()、reversed()などのリスト操作が必要な場合は、ジェネレーターを使用しないでください。

遅延評価が必要ない場合もあります(たとえば、リソースを解放できるように、すべての計算を前もって行う)。その場合、リスト式の方が良いかもしれません。


25
また、すべての計算を事前に実行することで、リスト要素の計算で例外がスローされた場合、後で繰り返されるループではなく、リストが作成されたポイントで例外がスローされます。続行する前にリスト全体のエラーのない処理を保証する必要がある場合、ジェネレーターは適切ではありません。
ライアンC.トンプソン

4
それは良い点です。ジェネレーターの処理の途中ですべてが爆発するだけで、非常にイライラします。潜在的に危険な場合があります。
David Eyk 2011年

26

プロファイル、プロファイル、プロファイル。

コードのプロファイリングは、実行していることが何らかの効果があるかどうかを知る唯一の方法です。

xrange、ジェネレーターなどのほとんどの使用法は、静的サイズ、小さなデータセットを超えています。それが本当に違いを生むのは、大きなデータセットに到達したときだけです。range()とxrange()は、ほとんどの場合、コードを少し醜く見せ、何も失わず、何かを得るだけの問題です。

プロファイル、プロファイル、プロファイル。


1
確かに、プロファイル。ある日、私は経験的な比較を試みます。それまでは、他の誰かがすでに持っていることを望んでいました。:)
David Eyk

プロファイル、プロファイル、プロファイル。同意します。プロファイル、プロファイル、プロファイル。
ジェッペ

17

あなたは賛成することはありませんzip以上iziprange以上のxrange発電機の内包表記上、またはリストの内包。Python 3.0でrangeありxrange様セマンティクスをし、zip持っているizip様セマンティクスを。

リスト内包表記はlist(frob(x) for x in foo)、実際のリストが必要な場合と同様に、実際にはより明確です。


3
@スティーブン私は同意しませんが、あなたの答えの背後にある理由は何であるか疑問に思っています。zip、範囲、およびリスト内包表記が、対応する「遅延」バージョンよりも優先されないのはなぜですか?
mhawke 2008年

彼が言ったように、zipとrangeの古い動作はすぐになくなるからです。

@スティーブン:良い点。3.0でのこれらの変更については忘れていました。これは、おそらく、そこにいる誰かが彼らの一般的な優位性を確信していることを意味します。Re:リスト内包表記は、多くの場合、より明確です(そして、拡張forループよりも高速です!)が、理解できないリスト内包表記を簡単に書くことができます。
David Eyk

9
私はあなたが何を意味するのかわかりますが、[]フォームは十分に説明的であると思います(そして、一般的に、より簡潔で、雑然としていません)。しかし、これは好みの問題です。
David Eyk

4
リスト操作はデータサイズが小さいほど高速ですが、データサイズが小さいとすべてが高速になるため、リストを使用する特別な理由がない限り、常にジェネレーターを選択する必要があります(そのような理由については、Ryan Ginstromの回答を参照してください)。
ライアンC.トンプソン

7

あなたが言うように、「これは特に大きなデータセットにとって意味があります」、これはあなたの質問に答えると思います。

パフォーマンスの面で壁にぶつかっていない場合でも、リストと標準機能に固執することができます。次に、パフォーマンスに問題が発生した場合は、切り替えを行います。

ただし、コメントで@ u0b34a0f6aeが述べているように、最初にジェネレーターを使用すると、より大きなデータセットに簡単にスケーリングできます。


5
+1ジェネレーターを使用すると、予期せずに大きなデータセットに対応できるようになります。
u0b34a0f6ae 2009年

6

パフォーマンスに関して:psycoを使用する場合、リストはジェネレーターよりもかなり高速になる可能性があります。以下の例では、psyco.full()を使用すると、リストがほぼ50%高速になります。

import psyco
import time
import cStringIO

def time_func(func):
    """The amount of time it requires func to run"""
    start = time.clock()
    func()
    return time.clock() - start

def fizzbuzz(num):
    """That algorithm we all know and love"""
    if not num % 3 and not num % 5:
        return "%d fizz buzz" % num
    elif not num % 3:
        return "%d fizz" % num
    elif not num % 5:
        return "%d buzz" % num
    return None

def with_list(num):
    """Try getting fizzbuzz with a list comprehension and range"""
    out = cStringIO.StringIO()
    for fibby in [fizzbuzz(x) for x in range(1, num) if fizzbuzz(x)]:
        print >> out, fibby
    return out.getvalue()

def with_genx(num):
    """Try getting fizzbuzz with generator expression and xrange"""
    out = cStringIO.StringIO()
    for fibby in (fizzbuzz(x) for x in xrange(1, num) if fizzbuzz(x)):
        print >> out, fibby
    return out.getvalue()

def main():
    """
    Test speed of generator expressions versus list comprehensions,
    with and without psyco.
    """

    #our variables
    nums = [10000, 100000]
    funcs = [with_list, with_genx]

    #  try without psyco 1st
    print "without psyco"
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

    #  now with psyco
    print "with psyco"
    psyco.full()
    for num in nums:
        print "  number:", num
        for func in funcs:
            print func.__name__, time_func(lambda : func(num)), "seconds"
        print

if __name__ == "__main__":
    main()

結果:

without psyco
  number: 10000
with_list 0.0519102208309 seconds
with_genx 0.0535933367509 seconds

  number: 100000
with_list 0.542204280744 seconds
with_genx 0.557837353115 seconds

with psyco
  number: 10000
with_list 0.0286369007033 seconds
with_genx 0.0513424889137 seconds

  number: 100000
with_list 0.335414877839 seconds
with_genx 0.580363490491 seconds

1
これは、psycoがジェネレーターをまったく高速化しないため、ジェネレーターよりもpsycoの欠点の方が多いためです。しかし、良い答えです。
Steven Huwig 2008年

4
また、psycoは現在ほとんどメンテナンスされていません。すべての開発者は、私の知る限りジェネレーターを最適化するPyPyのJITに時間を費やしています。
ヌーファルイブラヒム

3

パフォーマンスに関しては、ジェネレーターではなくリストを使用したいと思うことはありません。


all(True for _ in range(10 ** 8))Python3.8よりも遅いですall([True for _ in range(10 ** 8)])。私はここでジェネレーターよりもリストを好む
ggorlen

3

ジェネレーターがあなたがやろうとしていることを妨げるような状況を私は見つけたことがありません。ただし、ジェネレーターを使用しても、ジェネレーターを使用しない以外に役に立たない場合はたくさんあります。

例えば:

sorted(xrange(5))

以下を改善するものはありません。

sorted(range(5))

4
range(5)結果のリストはすでにソートされているため、どちらも改善を提供しません。
dan04 2014年

3

後で何か他のもののために値を保持する必要があり、セットのサイズが大きすぎない場合は、リスト内包を優先する必要があります。

例:プログラムの後半で数回ループするリストを作成しています。

ある程度、ジェネレーターは反復(ループ)の代わりと見なすことができますが、リスト内包表記はデータ構造の初期化の一種と考えることができます。データ構造を保持したい場合は、リスト内包表記を使用してください。


ストリームで限られた先読み/後読みだけが必要な場合は、おそらくitertools.tee()あなたを助けることができます。ただし、一般に、複数のパス、またはいくつかの中間データへのランダムアクセスが必要な場合は、そのリスト/セット/辞書を作成します。
Beni Cherniavsky-Paskin
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.