1

さて、とにかくこれについて心配する必要はないかもしれませんが、フィルターやマップなどのセットを介して可能性の(おそらく非常に長い、おそらく非常に短い)リストを渡すことを目的としたコードがあります、そして、私の実装がうまく機能するかどうか知りたいです。

私がやりたいことのタイプの例として、この一連の操作を考えてみましょう。

  • 1から100までのすべての数値を取得します
  • 偶数のものだけを保持します
  • 各数を二乗
  • 上記のリストにiがあり、[1、2、3、4、5]にjがあるすべてのペア[i、j]を生成します。
  • i +j>40のペアのみを保持します

さて、このナンセンスをすべて実行した後、このペアのセット[i、j]を調べて、特定の条件を満たすペアを探します。通常、ソリューションは最初のエントリの1つです。その場合、他のエントリも調べません。ただし、リスト全体を消費しなければならない場合があり、答えが見つからず、エラーをスローする必要があります。

「操作のチェーン」をジェネレーターのシーケンスとして実装したいと思います。つまり、各操作は前のジェネレーターによって生成されたアイテムを繰り返し、アイテムごとに独自の出力を「生成」します(SICPストリーム)。そうすれば、出力の最後の300エントリを見たことがなければ、それらは処理されません。itertoolsは、実行したい多くの種類の操作を実行するためのimapやifilterなどを提供することを知っていました。

私の質問は、一連のネストされたジェネレーターは、すべての可能性を反復処理する必要がある場合に、パフォーマンスに大きな打撃を与えるでしょうか?

4

3 に答える 3

3

2つの実装を試しました。1つはジェネレーターを使用し、もう1つはジェネレーターを使用しません。2.7でテストしたのでrange、イテレータではなくリストを返します。

これが実装です

ジェネレーターの使用

def foo1():
    data = ((a,b) for a in (i*i for i in xrange(1,101) if i%2) for b in [1,2,3,4,5] if a+b > 40)
    return list(data)

ジェネレータなし

def foo2():
    result=[]
    for i in range(1,101):
        if i%2:
            i=i*i
            for j in [1,2,3,4,5]:
                if i+j > 40:
                    result+=[(i,j)]
    return result

リストを追加しないように両方を混合する

def foo3():
    data=[(a,b) for a in (i*i for i in range(1,101)) for b in [1,2,3,4,5] if a+b > 40] 
    return data

一時リストの作成

def foo4():
    data=[(a,b) for a in [i*i for i in range(1,101)] for b in [1,2,3,4,5] if a+b > 40]
    return data

これが私の結果です

>>> t1=timeit.Timer("foo1()","from __main__ import foo1")
>>> t2=timeit.Timer("foo2()","from __main__ import foo2")
>>> t3=timeit.Timer("foo3()","from __main__ import foo3")
>>> t4=timeit.Timer("foo4()","from __main__ import foo4")

>>> print "%.2f usec/pass" % (1000000 * t1.timeit(number=10000)/10000)
100.95 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t2.timeit(number=10000)/10000)
158.90 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t3.timeit(number=10000)/10000)
130.02 usec/pass
>>> print "%.2f usec/pass" % (1000000 * t4.timeit(number=10000)/10000)
133.68 usec/pass
>>> 

結論:

ジェネレータ式は強力であり、さらに大幅に最適化できます。最も遅い例でわかるようにfoo2、パフォーマンスを損なう単一のリストを追加するのに苦労しました。foo3ほぼ同じ時間でfoo4あるため、一時リストの作成はボトルネックではなかったようです。これは、反復全体で1回だけ作成されたためです。ジェネレーターがないと、リストの追加や一時リストの作成など、パフォーマンスの問題がすぐに発生します。そのため、これらのパフォーマンスのボトルネックを克服するために、遅延評価が必要になりました。

于 2012-04-13T16:06:51.460 に答える
2

公式ドキュメントによると、ジェネレータ式を使用するimapことは、イテレータを作成するという点で基本的に呼び出すことと同じです。("ジェネレータ式は新しいジェネレータオブジェクトを生成します。 ")ネストされた式が個別の(構成された)オブジェクトを作成するのか、内部に複雑なロジックを持つ単一の式を作成するのかについての明確な議論はありませんが、インタプリタの実装者として自分を想像すると、ネストされたオブジェクトが最も多いようですネストされたジェネレータ式を実装する簡単な方法。

ただし、何がより良いパフォーマンスになるかを決定する際には、他の要因が関係しています。短命のオブジェクトの作成を最小限に抑えることがパフォーマンスの大きな要因であり、Pythonではそれを行っているときに気付くのが難しい場合があることを学びました。

パフォーマンスが悪い:(f(x) for x in range(100)) # builds 100-element list

よりよい性能:(f(x) for x in xrange(100)) # uses counting iterator

私は自分の実装で常にモジュールから、imapifilter使用していますが、それらはうまく機能していることがわかりました。これらを呼び出すたびに新しいイテレータオブジェクトが作成されますが、これはかなり軽量で、複数のアイテムが含まれることのないリストのようなものです。さらに、CPythonでは、これらはCで実装されているため、非常に効率的です。izipitertools

裏で、純粋なPythonで実装されたイテレータには、next各データを取得するために呼び出されるメソッドがあります。メソッド呼び出しのコストはそれほど高くありませんが、ゼロでもありません。したがって、コードを可能な限り最適化する必要のあるタイトなループで使用する場合は、次の提案があります。

  • 間違いなく、を使用しimap、可能な場合は、ifilterizipは対照的にmap、メモリ内の結果のリストfilterzip作成して返します。リストベースのバージョンを使用するコードがある場合は、イテレータベースのバージョンに変更することで大幅な改善が見られます。
  • このitertoolsモジュールには、、、などの他の関数が含まれtakewhileておりstarmap、連鎖イテレータの実装で一般的に役立ちます。chainchain.from_iterable
  • の複数のアプリケーションを連鎖させるのではなくifilter、可能な場合は渡された関数を組み合わせます。たとえば、の代わりにifilter(lambda v: v > 0, ifilter(lambda v: v % 3 == 0, data))、フィルタを。として組み合わせifilter(lambda v: (v > 0) and (v % 3 == 0), data)ます。場合によっては、この方法で操作を折りたたむことができるように、操作の順序を並べ替えることが有効な場合があります。
  • 副作用を達成するためにマップ操作を実行し、結果に関心がない場合は、代わりにこれを使用してmap、結果がメモリに蓄積されないようにすることができます。

    def consume(i):
      u'eat all contents of given iterator'
      while True:
        i.next()
    
    consume(imap(side_effect, data))
    

最後に、メモリ使用量を増やしたり、オブジェクトを不必要に繰り返し作成および破棄したりして、ガベージコレクタにストレスを与える可能性のある他の落とし穴に注意してください。これは実際にはイテレータとは何の関係もありませんが、パフォーマンスには影響します。以下の関数は、メモリ内にラムダ式を作成し、呼び出されるたびにそれを破棄します。

def foo(data):
  return reduce(R, imap(bar, ifilter(lambda v: v % 5 == 0, data)))

これを修正する1つの方法(これは毎回2つのイテレータオブジェクトを作成しますが、これは必要ですが、追加のラムダ式は作成されません):

_mod5zero = lambda v: v % 5 == 0
def foo(data):
  return reduce(R, imap(bar, ifilter(_mod5zero, data)))

(注:回答はPython 2に適用されます。Python3mapでは、イテレーターfilterを返します。)zip

于 2012-04-13T16:01:11.113 に答える
1

「ネストされた」イテレータは、イテレータが実装する関数の構成に相当するため、一般に、特に新しいパフォーマンスの考慮事項はありません。

ジェネレータは怠惰であるため、あるシーケンスを繰り返し割り当てて別のシーケンスに変換する場合と比較して、メモリ割り当てを削減する傾向があることに注意してください。

于 2012-04-13T16:10:28.360 に答える