使用条件子句避免重复且昂贵的操作

考虑以下列表理解:

>>> 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+) 装饰器 。这样,由于输入 xf 输出已经计算过一次,原始列表推导的第二个函数调用将与字典查找一样快。这种方法使用 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。

列表理解只生成一个列表一次,并将每个项目(从其原始居住地点到结果列表)复制一次。