在这个 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 有啥意义?的主要内容,如果未能解决你的问题,请参考以下文章

自定义迭代器和 itertools.tee 问题

python模块分析之itertools

一日一库—itertools

85.单继承

85.单继承

stm32的地址分配方面的问题求解,恳请大神细心指针,不胜感激~~~