列表理解过滤-“ set()陷阱”


68

一个合理的常见操作是list基于另一个过滤list。人们很快发现:

[x for x in list_1 if x in list_2]

对于大输入而言,速度很慢-为O(n * m)。uck 我们如何加快速度?使用aset进行过滤查找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)]

阿克!这也是O(n * m),因为pythonset(list_2) 每次都构建,而不仅仅是一次构建。


我以为故事就此结束了-python无法优化它,只能构建set一次。只是要注意陷阱。要忍受它。嗯

#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)可以优化掉设置的文字。它比f()这种情况下甚至更快,大概是因为它可以用代替LOAD_GLOBALa 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可以优化设置的文字以仅构建一次,但不能构建一次set(list_2)


7
感谢您引起我对这一细节的关注。
Hyperboreus

3
f如果您使用冻结集而不是集合,则似乎要快一些。
asmeurer 2013年

这有点晚了,但是如果您同时还没有弄清楚:您也可以dis.dis在内部代码对象上使用,只需从外部代码对象的co_consts中挖掘代码对象即可。例如。f = lambda: {a for b in c}; dis.dis(f.func_code.co_consts[1])
Aleksi Torhamo 2014年

仅供参考,此错误的结果是引入了设置字面量优化:bugs.python.org/issue6690,已在Py3开发期间解决,但未反向移植。
ShadowRanger 2015年

Answers:


51

为了进行优化set(list_2),解释器需要证明list_2(及其所有元素)在迭代之间没有变化。在一般情况下,这是一个难题,如果口译员甚至不尝试解决它,也不会令我感到惊讶。

另一方面,集合文字不能在迭代之间更改其值,因此已知优化是安全的。


6
令我吃惊的是,这为where在理解语法中引入子句提供了条件,以便可以在表达式中声明变量。
Marcin

确实,您可以做出list_2在理解过程中发生变化的病理。
2014年

然而,解释器甚至没有优化set([0,1,2,...,19])-运行速度与一样慢set(list_2)
Zaz

嗯,解释器还需要证明setfn在两次迭代之间没有变化。这就是为什么它没有优化set([0,1,2,...,19])
Zaz

39

Python 3.2的新功能开始

Python的窥孔优化器现在可以识别模式,例如x in {1, 2, 3}对一组常量中的成员资格进行测试。优化器将集合重铸为冻结集合,并存储预建常量。


大概它也是可传递的,较早地认识到它list2是整数的恒定范围,因此set(list2)也可以替换。
chepner

1
这不能回答以下问题:“为什么python 3.x可以优化掉set文字以仅生成一次,而不能生成一次set(list_2)?” 引号没有回答为什么不能针对进行相同的优化set(list_2)
Bakuriu

4
@martineau是的,但是为什么它不能再识别?我的意思是:这就像说“ python优化集合文字而不是其他东西,因为它仅优化集合文字”。他只是在确认其他案例未得到优化的前提下,确认了OP是正确的。原因并不难:因为集合文字是一个编译时间常数,而set对它的调用则不是,并且解释器没有太多时间在运行时进行优化。这是不是在答复中提到。
Bakuriu

2
编译器永远无法优化,set(x)因为它set直到运行时才知道名称将绑定到什么名称。
asmeurer

2
另一个问题:计算对象集通常会产生副作用。该对象可能是耗尽的迭代器。对象__hash__可以做任何事情。我认为这里的示例仅适用于因为它是一组文字。
asmeurer

18

所以现在我想知道-为什么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是您认为的那样。有时,您可以在编译时的限制性条件下执行此操作,或者在运行时更方便地执行此操作,但这绝对是不简单的。

这有点好笑:通常当提出进行这样的优化的建议时,一个推论就是它是如此的好,这使得人们更难于推断Python的性能,甚至是算法。您的问题为该异议提供了一些证据。


基本上,每个可变对象都需要在其中添加一个计数器,只要该对象被修改,该计数器就会增加。然后set(mylist)可以记住mylistset构建时具有相同计数器值的地方,即尚未修改。这将增加大量操作(每项或每个属性的分配)的开销,而这些功能根本就不会经常使用。
kindall 2013年

13

评论太久

这不会说明优化细节或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:
    # ...

10

根本原因是文字确实不能更改,而如果是类似表达式set(list_2),则评估目标表达式或可理解的迭代可能会更改。的值set(list_2)。例如,如果您有

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

有可能进行f修改list_2

即使是简单的[x for x in blah ...]表达,理论上也可以修改的__iter__方法。blahlist_2

我可以想象有一些优化的范围,但是当前的行为使事情变得更简单。如果您开始为诸如“如果目标表达式是单个裸名并且可迭代对象是内置列表或dict ...仅被评估一次”之类的东西添加优化,您将很难弄清楚在任何情况下会发生什么。给定情况。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.