web-dev-qa-db-ja.com

リスト内包フィルタリング-「set()トラップ」

かなり一般的な操作は、あるlistを別のlistに基づいてフィルタリングすることです。人々はすぐにこれを見つけます:

_[x for x in list_1 if x in list_2]
_

入力が大きい場合は低速です-O(n * m)です。うん。これをどのようにスピードアップしますか? setを使用して、フィルタリングルックアップを作成しますO(1):

_s = set(list_2)
[x for x in list_1 if x in s]
_

これにより、全体的にニースのO(n)の動作が得られます。ただし、ベテランのコーダーでさえThe Trap™に分類されることがよくあります。

_[x for x in list_1 if x in set(list_2)]
_

わかった! pythonビルドset(list_2)every時間、1回だけではないため、これもO(n * m)です。


これで話は終わりだと思いました-python最適化してsetを1回だけ構築することはできません。落とし穴に注意してください。一緒に暮らす必要があります。うーん。

_#python 3.3.2+
list_2 = list(range(20)) #small for demonstration purposes
s = set(list_2)
list_1 = list(range(100000))
def f():
    return [x for x in list_1 if x in s]
def g():
    return [x for x in list_1 if x in set(list_2)]
def h():
    return [x for x in list_1 if x in {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19}]

%timeit f()
100 loops, best of 3: 7.31 ms per loop

%timeit g()
10 loops, best of 3: 77.4 ms per loop

%timeit h()
100 loops, best of 3: 6.66 ms per loop
_

ええと、python(3.3)canセットリテラルを最適化してください。この場合はf()よりもさらに高速です。おそらく、置き換えられるためです。 _LOAD_GLOBAL_と_LOAD_FAST_。

_#python 2.7.5+
%timeit h()
10 loops, best of 3: 72.5 ms per loop
_

Python 2は、特にこの最適化を行いません。 python3が何をしているのかをさらに調査しようとしましたが、残念ながら_dis.dis_は理解式の内部を調べることができません。基本的に、興味深いものはすべて_MAKE_FUNCTION_に変わります。

だから今私は疑問に思っています-なぜpython 3.xは、セットリテラルを最適化して1回だけビルドし、set(list_2)はビルドしないのですか?

65
roippi

set(list_2)を最適化するために、インタプリタはlist_2(およびそのすべての要素)は反復間で変化しません。これは一般的なケースでは難しい問題であり、通訳がそれに取り組もうとさえしなくても私は驚かないでしょう。

一方、セットリテラルは反復間でその値を変更できないため、最適化は安全であることがわかっています。

49
NPE

From What’s New In Python 3.2

Pythonののぞき穴オプティマイザーは、x in {1, 2, 3}などのパターンを一連の定数のメンバーシップのテストとして認識するようになりました。オプティマイザーは、セットをフリーズセットとして再キャストし、事前に作成された定数を保管します。

39

だから今私は疑問に思っています-なぜpython 3.xはsetリテラルを最適化して一度だけビルドし、set(list_2)はビルドしないのですか?

この問題についてはまだ誰も言及していません。set([1,2,3]){1, 2, 3}が同じものであることをどうやって知っていますか?

>>> import random
>>> def set(arg):
...     return [random.choice(range(5))]
... 
>>> list1 = list(range(5))
>>> [x for x in list1 if x in set(list1)]
[0, 4]
>>> [x for x in list1 if x in set(list1)]
[0]

リテラルをシャドウイングすることはできません。 setをシャドウすることができます。したがって、巻き上げを検討する前に、list1が影響を受けていないことだけでなく、setが自分の考えているものであることを確認する必要があります。コンパイル時の制限された条件下で、または実行時により便利に、それを実行できる場合もありますが、それは間違いなく重要です。

これはちょっとおかしいです。このような最適化を行うという提案が出たとき、1つの反発は、それが素晴らしいので、Pythonパフォーマンスがどうなるかについて推論するのが難しくなるということです)あなたの質問は、この異議のいくつかの証拠を提供します。

18
DSM

コメントするには長すぎます

これは、最適化の詳細やv2とv3の違いについては説明しません。しかし、状況によってはこれに遭遇すると、データオブジェクトからコンテキストマネージャーを作成すると便利です。

class context_set(set):
    def __enter__(self):
        return self
    def __exit__(self, *args):
        pass

def context_version():
    with context_set(list_2) as s:
        return [x for x in list_1 if x in s]

これを使用すると、次のようになります。

In [180]: %timeit context_version()
100 loops, best of 3: 17.8 ms per loop

場合によっては、理解の前にオブジェクトを作成することと、理解の中でオブジェクトを作成することの間に素晴らしい一時的なギャップを提供し、必要に応じてカスタムの分解コードを許可します。

contextlib.contextmanagerを使用して、より一般的なバージョンを作成できます。これが私が言っていることの手っ取り早いバージョンです。

def context(some_type):
    from contextlib import contextmanager
    generator_apply_type = lambda x: (some_type(y) for y in (x,))
    return contextmanager(generator_apply_type)

次に、次のことができます。

with context(set)(list_2) as s:
    # ...

または同じくらい簡単に

with context(Tuple)(list_2) as t:
    # ...
13
ely

基本的な理由は、リテラルは実際には変更できないためですが、set(list_2)のような式の場合、ターゲット式または理解の反復可能性を評価すると、set(list_2)の値が変更される可能性があります。 。たとえば、

[f(x) for x in list_1 if x in set(list_2)]

flist_2を変更する可能性があります。

単純な[x for x in blah ...]式の場合でも、理論的にはblah__iter__メソッドがlist_2を変更する可能性があります。

最適化の余地はあると思いますが、現在の動作では物事が単純になっています。 「ターゲット式が単一の裸の名前であり、反復可能オブジェクトが組み込みリストまたはdictである場合に一度だけ評価される」などの最適化を追加し始めると、どのような場合に何が起こるかを理解するのがはるかに複雑になります。与えられた状況。

10
BrenBarn