为什么不为“ in”操作设置文字O(1)?

时间:2019-02-11 23:27:32

标签: python set tuples literals membership

通过对一个变量测试很多常量是很常见的

if x in ('foo', 'bar', 'baz'):

而不是

if x == 'foo' or x == 'bar' or x == 'baz':

我已经看到很多“使用{'foo', 'bar', 'baz'}而不是('foo', 'bar', 'baz')来提高O(1)的性能,这是有道理的,但是测试显示出非常奇怪的结果。

%timeit 1 in {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
27.6 ns ± 2.35 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%timeit 10 in {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
136 ns ± 4.04 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

%timeit 0 in {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
186 ns ± 26.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

为什么对设置文字的查询不是固定时间?

5 个答案:

答案 0 :(得分:2)

好,这里有几件事。

  1. set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])非常慢,因为它可能会首先建立列表。我想在3.7+中会有一些优化,但是无论如何。因此,设置文字更快。
  2. “检查第一个成员的速度甚至要慢一些”(关于集合的问题),这并不是神奇的O(1)。集合成员检查是哈希+模+哈希的比较+冲突/删除的回退。没有“第一成员”这样的东西。
  3. 在小数据上,组合的表现要优于集合-因为集合利用了很多机制。它是O(1),但常数在某些范围内高于O(N)的值。使用10 ** 6长度的代码来分析您的代码,您会发现其中的区别
  4. 使用文字进行计时是很奇怪的想法,通常快速的成员资格检查会利用已经创建的容器:

    t = tuple(range(10**6))
    s = set(range(10**6))
    %timeit 999999 in t
    11.9 ms ± 92 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    
    %timeit 999999 in s
    52 ns ± 0.538 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
    

关于测试渐进复杂度的注释-您应始终检查增长幅度,原始数据毫无意义。即

x = 1; t = tuple(range(10**x)); s = set(range(10**x))
%timeit (-1) in t
168 ns ± 22.9 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
%timeit (-1) in s
38.3 ns ± 0.46 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

x = 2; t = tuple(range(10**x)); s = set(range(10**x))
%timeit (-1) in t
1.1 µs ± 17.1 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit (-1) in s
37.7 ns ± 0.101 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

x = 4; t = tuple(range(10**x)); s = set(range(10**x))
%timeit (-1) in t
107 µs ± 860 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit (-1) in s
39 ns ± 1.66 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

x = 6; t = tuple(range(10**x)); s = set(range(10**x))
%timeit (-1) in t
10.8 ms ± 114 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit (-1) in s
38 ns ± 0.333 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

所以您可以在这里清楚地看到线性与常数。

答案 1 :(得分:2)

您正在测试集合的构造。让我们再次尝试实验,但是只构造一次a。首先,这是一个元组:

$ python -m timeit -s 'a = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)' -- '0 in a'
10000000 loops, best of 5: 22.6 nsec per loop

搜索最后一个元素比较慢:

$ python -m timeit -s 'a = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)' -- '9 in a'
2000000 loops, best of 5: 136 nsec per loop

正在搜索缺失值:

$ python -m timeit -s 'a = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)' -- '-1 in a'
2000000 loops, best of 5: 132 nsec per loop
一旦构造了对象,

set.__contains__就会好得多:

$ python -m timeit -s 'a = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}' -- '0 in a'
10000000 loops, best of 5: 26.3 nsec per loop

按预期,顺序无关紧要:

$ python -m timeit -s 'a = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}' -- '9 in a'
10000000 loops, best of 5: 26.1 nsec per loop

也不检查缺失值:

$ python -m timeit -s 'a = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}' -- '-1 in a'
10000000 loops, best of 5: 26.4 nsec per loop

答案 2 :(得分:1)

我没有得到您的结果:

python -m timeit "(-1) in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}"
10000000 loops, best of 3: 0.0238 usec per loop

python -m timeit "0 in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}"
10000000 loops, best of 3: 0.0235 usec per loop

python -m timeit "9 in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}"
10000000 loops, best of 3: 0.0208 usec per loop

关于您在set()创建和{}创建中的区别的问题,您可以看到字节码中的区别:

设置文字:

from dis import dis
print(dis("9 in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}"))

输出:

          0 LOAD_CONST               0 (9)
          2 LOAD_CONST              10 (frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}))
          4 COMPARE_OP               6 (in)
          6 RETURN_VALUE

使用功能:

print(dis("9 in set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])"))

输出:

          0 LOAD_CONST               0 (9)
          2 LOAD_NAME                0 (set)
          4 LOAD_CONST               1 (0)
          6 LOAD_CONST               2 (1)
          8 LOAD_CONST               3 (2)
         10 LOAD_CONST               4 (3)
         12 LOAD_CONST               5 (4)
         14 LOAD_CONST               6 (5)
         16 LOAD_CONST               7 (6)
         18 LOAD_CONST               8 (7)
         20 LOAD_CONST               9 (8)
         22 LOAD_CONST               0 (9)
         24 BUILD_LIST              10
         26 CALL_FUNCTION            1
         28 COMPARE_OP               6 (in)
         30 RETURN_VALUE

两者都构建了set,但是python能够立即将文字集识别为文字集(并优化以构建冻结集,因为它知道不需要任何添加和删除)要构建列表,请加载set函数,然后在列表上调用该函数。但是,这种差异仅在集合创建中。它不会影响in操作。

答案 3 :(得分:1)

设置查找平均是O(1)操作。它不应随您检查集合的哪个元素而一致地改变性能,除非在一定程度上是随机的,因为某些值可能与其他值发生哈希冲突,因此需要更长的时间才能找到。您在小型集合中查找不同值所看到的时间差几乎可以肯定是巧合,或者是您误认为数据了。

请注意,您不仅仅是在测试中计时集成员资格。您每次还会创建一个新集合,通常是一个O(N)操作(其中N是集合中值的数量)。在某些特殊情况下,可能会在O(1)时间创建集合文字,因为Python编译器会进行优化,以将可变的set对象替换为预先计算的不可变frozenset对象作为一个常数。这仅在编译器期望重新创建对象很多次并且可以告诉您对设置对象的引用不能泄漏出其运行的代码范围的情况下发生。例如,在理解或生成器表达式的if子句中使用的集合可以得到常量处理:

[foo(x) for x in some_iterable if x in {0, 1, 2, 3, 4, 5, 6, 7, 9}]

在最新版本的CPython中,此处的设置文字将始终引用常量frozenset,不需要为从x产生的每个some_iterable值重新创建。但是您可能不应该依赖此行为,因为其他Python解释器,甚至其他版本的CPython也可能无法执行相同的优化。

这无法解释您在计时中看到的内容。我怀疑您的环境中存在一些人工制品可以解释此问题,或者可能是随机的机会,即集合中的最小值恰好没有任何哈希冲突,而最后一个(巧合)有几次。如果您测试集合中的其他值,则可能会获得一小段不同的时序。但是该范围不会随集合元素的数量而变化很大,对于集合的每种大小,它应该相当相似(可能会有很小的差异,但要小于N倍)。

尝试进行更具体的测试(不考虑集合创建),如下所示:

import timeit, random

big_set = set(range(1000000))

for x in random.sample(range(1000000), 10):
    print('looking up', x, 'took', timeit.timeit(lambda: x in big_set), 'seconds')

答案 4 :(得分:0)

您似乎对算法复杂性的含义感到困惑-您尚未测试该特性。 复杂度描述了随着输入大小趋于无穷大而渐近的时间要求。

您的测试仅针对一种输入大小:10个元素。您测试最佳和最差情况。但是,要解决算法的复杂性,您需要从时序中提取初始化步骤,然后比较各种输入大小的性能:可能是10的幂,范围是10到10 ** 12。