我正在阅读itertools recipe for unique_everseen:
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in filterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
在上面的代码中定义seen_add = seen.add
有什么意义?
答案 0 :(得分:3)
性能。使用本地名称取消引用该方法要比属性查找(每次必须绑定一个新方法对象)快得多:
>>> import timeit
>>> timeit.timeit('s.add', 's = set()', number=10**7)
0.4227792940218933
>>> timeit.timeit('seen_add', 's = set(); seen_add = s.add', number=10**7)
0.15441945398924872
使用本地参考的速度几乎快3倍。由于set.add
用于循环,因此值得优化属性查找。
答案 1 :(得分:2)
这是一种名为"hoisting" or "Loop-invariant code motion"的技术。本质上,您执行多次执行的操作,但始终在循环外部而不是在循环体中返回相同的值。
在这种情况下,循环将重复查找add
集的seen
属性,并创建一个"绑定方法"。这实际上非常快,但仍然是一个在循环内执行多次的操作,并且总是给出相同的结果。所以你可以一次查找属性(在这种情况下是绑定方法)并将其存储在变量中以获得一些性能。
请注意,虽然这提供了加速,但绝不是"很多"。为了这个时间,我删除了第二个分支以缩短代码:
from itertools import filterfalse
def unique_everseen(iterable):
seen = set()
seen_add = seen.add
for element in filterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
def unique_everseen_without(iterable):
seen = set()
for element in filterfalse(seen.__contains__, iterable):
seen.add(element)
yield element
一些示例性时间:
# no duplicates
a = list(range(10000))
%timeit list(unique_everseen(a))
# 5.73 ms ± 279 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit list(unique_everseen_without(a))
# 6.81 ms ± 396 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# some duplicates
import random
a = [random.randint(0, 100) for _ in range(10000)]
%timeit list(unique_everseen(a))
# 1.64 ms ± 12.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit list(unique_everseen_without(a))
# 1.66 ms ± 16.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# only duplicates
a = [1]*10000
%timeit list(unique_everseen(a))
# 1.64 ms ± 78.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit list(unique_everseen_without(a))
# 1.63 ms ± 24.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
因此,当你在没有重复的情况下获得~10%的加速时,如果你有很多重复,它实际上几乎没用。
实际上这个食谱显示了另一个"吊装"的例子,更具体地说是filterfalse(seen.__contains__, iterable)
。这会查找__contains__
次设置的seen
方法,并在filterfalse
内重复调用它。
也许外卖应该是:提升方法查找是一种微优化。它减少了循环的常数因子。在某些操作中加速可能是值得的,但我个人认为它应该谨慎使用,并且只能与分析/基准测试结合使用。