使用條件子句避免重複且昂貴的操作
考慮以下列表理解:
>>> 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。
列表理解只生成一個列表一次,並將每個專案(從其原始居住地點到結果列表)複製一次。