生成器表达式的循环肯定比列表慢。但在这种情况下,生成器内的迭代基本上是列表本身的循环,因此对生成器的next() 调用基本上委托给列表的next() 方法。
例如,在这种情况下,没有 2 倍的性能差异。
>>> lst = list(range(10**5))
>>> %%timeit
... sum(x for x in lst)
...
100 loops, best of 3: 6.39 ms per loop
>>> %%timeit
... c = 0
... for x in lst: c += x
...
100 loops, best of 3: 6.69 ms per loop
首先让我们检查两种方法的字节码:
def gt_0(lst):
for elm in lst:
if elm > 0:
return True
return False
def any_with_ge(lst):
return any(elm > 0 for elm in lst)
字节码:
>>> dis.dis(gt_0)
10 0 SETUP_LOOP 30 (to 33)
3 LOAD_FAST 0 (lst)
6 GET_ITER
>> 7 FOR_ITER 22 (to 32)
10 STORE_FAST 1 (elm)
11 13 LOAD_FAST 1 (elm)
16 LOAD_CONST 1 (0)
19 COMPARE_OP 4 (>)
22 POP_JUMP_IF_FALSE 7
12 25 LOAD_GLOBAL 0 (True)
28 RETURN_VALUE
29 JUMP_ABSOLUTE 7
>> 32 POP_BLOCK
13 >> 33 LOAD_GLOBAL 1 (False)
36 RETURN_VALUE
>>> dis.dis(any_with_ge.func_code.co_consts[1])
17 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 1 (None)
26 RETURN_VALUE
正如您所见,any() 版本中没有跳转条件,它基本上获取了> 比较的值,然后再次使用PyObject_IsTrue 再次检查其真实值。另一方面,gt_0 会检查一次条件的真值,并据此返回True 或False。
现在让我们添加另一个基于 any() 的版本,它具有类似 for 循环中的 if 条件。
def any_with_ge_and_condition(lst):
return any(True for elm in lst if elm > 0)
字节码:
>>> dis.dis(any_with_ge_and_condition.func_code.co_consts[1])
21 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 23 (to 29)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 POP_JUMP_IF_FALSE 3
21 LOAD_GLOBAL 0 (True)
24 YIELD_VALUE
25 POP_TOP
26 JUMP_ABSOLUTE 3
>> 29 LOAD_CONST 1 (None)
32 RETURN_VALUE
现在我们通过添加条件减少了any() 所做的工作(查看最后一节了解更多详细信息),当条件为True 时,它只需要检查两次真值,否则它将基本上跳到下一项。
现在让我们比较一下这 3 个的时间安排:
>>> %timeit gt_0(lst)
10 loops, best of 3: 26.1 ms per loop
>>> %timeit any_with_ge(lst)
10 loops, best of 3: 57.7 ms per loop
>>> %timeit any_with_ge_and_condition(lst)
10 loops, best of 3: 26.8 ms per loop
让我们修改gt_0 以包含两个检查,就像在简单的any() 版本中一样,并检查其时间。
from operator import truth
# This calls `PyObject_IsTrue` internally
# https://github.com/python/cpython/blob/master/Modules/_operator.c#L30
def gt_0_truth(lst, truth=truth): # truth=truth to prevent global lookups
for elm in lst:
condition = elm > 0
if truth(condition):
return True
return False
时间:
>>> %timeit gt_0_truth(lst)
10 loops, best of 3: 56.6 ms per loop
现在,让我们看看当我们尝试使用 operator.truth 两次检查项目的真值时会发生什么。
>> %%timeit t=truth
... [t(i) for i in xrange(10**5)]
...
100 loops, best of 3: 5.45 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**5)]
...
100 loops, best of 3: 9.06 ms per loop
>>> %%timeit t=truth
[t(i) for i in xrange(10**6)]
...
10 loops, best of 3: 58.8 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**6)]
...
10 loops, best of 3: 87.8 ms per loop
即使我们只是在一个已经是布尔值的对象上调用truth()(即PyObject_IsTrue),这还是有很大区别的,我想这可以解释基本any() 版本的缓慢性。
您可能会争辩说any() 中的if 条件也会导致两次真实性检查,但当comparison operation 返回Py_True 或Py_False 时情况并非如此。 POP_JUMP_IF_FALSE 只是跳转到下一个 OP 代码,不会调用 PyObject_IsTrue。