使用条件子句避免重复且昂贵的操作
考虑以下列表理解:
>>> def f(x):
... import time
... time.sleep(.1) # Simulate expensive function
... return x**2
>>> [f(x) for x in range(1000) if f(x) > 10]
[16, 25, 36, ...]
这导致两次调用 f(x)
为 1000 个 x
值:一次调用生成值,另一次调用 if
条件。如果 f(x)
是一项特别昂贵的操作,这可能会产生重大的性能影响。更糟糕的是,如果调用 f()
有副作用,它可能会产生令人惊讶的结果。
相反,你应该通过生成中间可迭代( 生成器表达式 )来为 x
的每个值仅评估一次昂贵的操作,如下所示:
>>> [v for v in (f(x) for x in range(1000)) if v > 10]
[16, 25, 36, ...]
或者,使用内置映射等效:
>>> [v for v in map(f, range(1000)) if v > 10]
[16, 25, 36, ...]
另一种可能导致代码更易读的方法是将部分结果(前一个示例中的 v
)放在可迭代(例如列表或元组)中,然后迭代它。由于 v
将是 iterable 中唯一的元素,结果是我们现在只对一次计算的慢函数的输出有一个引用:
>>> [v for x in range(1000) for v in [f(x)] if v > 10]
[16, 25, 36, ...]
但是,在实践中,代码逻辑可能更复杂,保持可读性非常重要。通常,建议在复杂的单线程上使用单独的发电机功能 :
>>> def process_prime_numbers(iterable):
... for x in iterable:
... if is_prime(x):
... yield f(x)
...
>>> [x for x in process_prime_numbers(range(1000)) if x > 10]
[11, 13, 17, 19, ...]
另一种防止计算 f(x)
的方法是在 f(x)
上使用 @functools.lru_cache()
(Python 3.2+) 装饰器 。这样,由于输入 x
的 f
输出已经计算过一次,原始列表推导的第二个函数调用将与字典查找一样快。这种方法使用 memoization来提高效率,这与使用生成器表达式相当。
假设你必须压扁一个列表
l = [[1, 2, 3], [4, 5, 6], [7], [8, 9]]
一些方法可能是:
reduce(lambda x, y: x+y, l)
sum(l, [])
list(itertools.chain(*l))
但是列表理解会提供最佳的时间复杂度。
[item for sublist in l for item in sublist]
当存在 L 个子列表时,基于+的快捷方式(包括隐含的总和)必然是 O(L ^ 2) - 随着中间结果列表持续变长,在每个步骤中新的中间结果列表对象获得已分配,并且必须复制先前中间结果中的所有项目(以及最后添加的一些新项目)。所以(为了简单而没有实际的失去一般性)说你有每个项目的 L 个子列表:第一个 I 项目来回复制 L-1 次,第二个 I 项目 L-2 次,依此类推; 总复制数是 I 乘以 x 的总和,从 1 到 L 排除,即 I *(L ** 2)/ 2。
列表理解只生成一个列表一次,并将每个项目(从其原始居住地点到结果列表)复制一次。