在这个itertools配方中定义seen_add = seen.add有什么意义?

时间:2017-09-26 08:56:57

标签: python itertools

我正在阅读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有什么意义?

2 个答案:

答案 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内重复调用它。

也许外卖应该是:提升方法查找是一种微优化。它减少了循环的常数因子。在某些操作中加速可能是值得的,但我个人认为它应该谨慎使用,并且只能与分析/基准测试结合使用。