在这个 itertools 配方中定义 seen_add = seen.add 有啥意义?
Posted
技术标签:
【中文标题】在这个 itertools 配方中定义 seen_add = seen.add 有啥意义?【英文标题】:What is the point of defining seen_add = seen.add in this itertools recipe?在这个 itertools 配方中定义 seen_add = seen.add 有什么意义? 【发布时间】:2017-09-26 08:56:56 【问题描述】:我正在阅读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
有什么意义?
【问题讨论】:
【参考方案1】:性能。使用本地名称取消引用方法比属性查找快得多(每次都必须绑定一个新的方法对象):
>>> 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
是循环使用的,所以值得优化掉属性查找。
【讨论】:
【参考方案2】:这是一种称为"hoisting" or "Loop-invariant code motion" 的技术。本质上,你做了一个多次执行的操作,但总是在循环外而不是循环体中返回相同的值。
在这种情况下,循环将重复查找 add
集合的 add
属性并创建一个“绑定方法”。这实际上非常快,但仍然是一个在循环内执行多次并且总是给出相同结果的操作。因此,您可以查找一次属性(在本例中为绑定方法)并将其存储在变量中以获得一些性能。
请注意,虽然这提供了加速,但绝不是“太多”。我为这个时间删除了第二个分支以使代码更短:
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)
。这会查找您的seen
集的__contains__
方法一次,并在filterfalse
内重复调用它。
也许要点应该是:提升方法查找是一种微优化。它减少了循环的常数因子。在某些操作中加速可能是值得的,但我个人认为应该谨慎使用它,并且只能与分析/基准测试结合使用。
【讨论】:
以上是关于在这个 itertools 配方中定义 seen_add = seen.add 有啥意义?的主要内容,如果未能解决你的问题,请参考以下文章