调用lambda n: n 这么多次的额外开销真的就是这么贵。
In [17]: key = lambda n: n
In [18]: x = [random() for _ in range(1234567)]
In [19]: %timeit nlargest(10, x)
33.1 ms ± 2.71 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [20]: %timeit nlargest(10, x, key=key)
133 ms ± 3.7 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [21]: %%timeit
...: for i in x:
...: key(i)
...:
93.2 ms ± 978 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [22]: %%timeit
...: for i in x:
...: pass
...:
10.1 ms ± 298 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
如您所见,对所有元素调用key 的成本几乎占了全部开销。
sorted 的关键评估成本同样高,但由于排序的总工作成本更高,关键调用的开销占总数的百分比更小。您应该将使用密钥的绝对开销与nlargest 或sorted 进行比较,而不是开销占基数的百分比。
In [23]: %timeit sorted(x)
542 ms ± 13.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [24]: %timeit sorted(x, key=key)
683 ms ± 12.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
如您所见,key 调用的成本约占在此输入上使用此键和 sorted 的开销的一半,其余开销可能来自在自行排序。
您可能想知道nlargest 是如何设法为每个元素做这么少的工作的。对于无键情况,大多数迭代发生在以下循环中:
for elem in it:
if top < elem:
_heapreplace(result, (elem, order))
top = result[0][0]
order -= 1
或者对于有钥匙的情况:
for elem in it:
k = key(elem)
if top < k:
_heapreplace(result, (k, order, elem))
top = result[0][0]
order -= 1
关键的认识是top < elem 和top < k 分支几乎从未被采用。一旦算法找到了 10 个相当大的元素,剩下的大部分元素将小于当前的 10 个候选元素。在需要替换堆元素的极少数情况下,这只会使其他元素更难通过调用heapreplace 所需的栏。
在随机输入上,nlargest 进行的 heapreplace 调用次数预计与输入大小成对数。具体来说,对于nlargest(10, x),除了x 的前10 个元素之外,元素x[i] 有10/(i+1) 的概率位于l[:i+1] 的前10 个元素中,这是heapreplace 调用的必要条件。通过期望的线性,heapreplace 调用的预期数量是这些概率的总和,并且该总和是 O(log(len(x)))。 (此分析适用于任何常数替换 10,但需要对 nlargest(n, l) 中的变量 n 进行更复杂的分析。)
对于已排序的输入,性能故事将大不相同,其中每个元素都将通过if 检查:
In [25]: sorted_x = sorted(x)
In [26]: %timeit nlargest(10, sorted_x)
463 ms ± 26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
是未分类案例的 10 倍以上!