使用條件子句避免重複且昂貴的操作

考慮以下列表理解:

>>> 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。

列表理解只生成一個列表一次,並將每個專案(從其原始居住地點到結果列表)複製一次。