【问题标题】:Why would anyone check 'x in list'?为什么有人会检查“列表中的 x”?
【发布时间】:2013-01-10 07:50:30
【问题描述】:

在 Python 中,使用in-操作符可以很容易地检查一个值是否包含在容器中。我想知道为什么有人会在列表上使用in-operator,但是,首先将列表转换为这样的集合会更有效:

if x in [1,2,3]:

相对

if x in set([1,2,3]):

查看time complexity 时,第一个有 O(n),而第二个则优于 O(1)。使用第一个的唯一原因是它更易读且写起来更短吗?或者有没有更实用的特殊情况?为什么 Python 开发人员没有通过首先将第一个转换为第二个来实现第一个?这不是 O(1) 复杂度吗?

【问题讨论】:

  • 列表到集合的底层转换的计算复杂度是多少?我希望 O(n),但我不知道 Python 是如何实现的。
  • 您真的认为somelist 很小的x in somelist 实际上比先将其转换为set 然后进行操作更糟糕吗?
  • 当集合是本机数据类型时,文档看起来像是在谈论 x in set 的复杂性。 set() 本身的复杂性是什么?是 O(n) 吗?
  • 另一个原因可能是您不能总是使用集合来代替列表。列表可以有重复项,集合没有。要使用的数据结构取决于您的数据是什么。
  • @ypercube 当您使用 in 运算符时没关系。

标签: python list set


【解决方案1】:
if x in set([1,2,3]):

if x in [1,2,3]:

将列表转换为集合需要遍历列表,因此至少需要 O(n) 时间。* 实际上,它比搜索项目花费的时间要长得多,因为它涉及散列然后插入每个项目。

当集合被转换一次然后检查多次时,使用集合是有效的。确实,通过在列表range(1000) 中搜索500 来尝试此操作表明,一旦你正在检查至少 3 次:

import timeit

def time_list(x, lst, num):
    for n in xrange(num):
        x in lst

def time_turn_set(x, lst, num):
    s = set(lst)
    for n in xrange(num):
        x in s

for num in range(1, 10):
    size = 1000
    setup_str = "lst = range(%d); from __main__ import %s"
    print num,
    print timeit.timeit("time_list(%d, lst, %d)" % (size / 2, num),
                        setup=setup_str % (size, "time_list"), number=10000),
    print timeit.timeit("time_turn_set(%d, lst, %d)" % (size / 2, num),
                        setup=setup_str % (size, "time_turn_set"), number=10000)

给我:

1 0.124024152756 0.334127902985
2 0.250166893005 0.343378067017
3 0.359009981155 0.356444835663
4 0.464100837708 0.38081407547
5 0.600295066833 0.34722495079
6 0.692923069 0.358560085297
7 0.787877082825 0.338326931
8 0.877299070358 0.344762086868
9 1.00078821182 0.339591026306

列表大小在 500 到 50000 之间的测试给出大致相同的结果。

* 实际上,在真正的渐近意义上,插入哈希表(并且就此而言,检查一个值)不是 O(1) 时间,而是线性 O(n) 时间的恒定加速(因为如果列表太大的碰撞会累积)。这将使set([1,2,3]) 操作处于O(n^2) 时间而不是O(n)。然而,在实践中,如果列表大小合理且实现良好,您基本上可以始终假设哈希表的插入和查找是O(1) 操作。

【讨论】:

  • 实际演示您答案的最后一个短语会很有趣。显示实际正确的尺寸。
  • 请注意,对于常量集 literals(还有其他优点,即更具可读性),Python 3.2+ 以与优化常量元组相同的方式优化它们——仅构建一次集合并将其烘焙到字节码中。
  • 哎呀。这对我来说很愚蠢 - 我错误地忽略了转换并隐含地假设这将是 O(1)。谢谢!
  • @mmgp:对于合理的列表大小范围,看起来像神奇的数字是如果列表/集合被检查 3 次(尽管对于非常小的列表或非常大的列表,这个数字会上升)。
【解决方案2】:

让我们测试一下你的假设:

In [19]: %timeit 1 in [1, 2, 3]
10000000 loops, best of 3: 52.3 ns per loop

In [20]: %timeit 4 in [1, 2, 3]
10000000 loops, best of 3: 118 ns per loop

In [21]: %timeit 1 in set([1, 2, 3])
1000000 loops, best of 3: 552 ns per loop

In [22]: %timeit 4 in set([1, 2, 3])
1000000 loops, best of 3: 558 ns per loop

因此,在您的确切示例中,使用 set() 比使用列表慢 5 到 10 倍。

仅创建集合需要 517 ns:

In [23]: %timeit set([1, 2, 3])
1000000 loops, best of 3: 517 ns per loop

让我们把创建集合的因素排除在检查之外:

In [26]: s = set([1, 2, 3])

In [27]: %timeit 1 in s
10000000 loops, best of 3: 72.5 ns per loop

In [28]: %timeit 4 in s
10000000 loops, best of 3: 71.4 ns per loop

这使得性能差异不那么明显。现在listset 的相对性能取决于提供给in 的确切值。如果它们出现在列表中并且靠近列表的开头,那么list 可能会获胜。否则,set 可能会获胜。

当然,如果in的右边更大,那么结论就会大不相同。

底线:

  1. 不要过早优化。
  2. 在优化之前始终分析实际输入。

【讨论】:

  • 我想知道你用来做这个测试的工具,我只是用 IDLE
【解决方案3】:

如果你想做微优化,你必须测量:

l.py:
for x in range(1000000):
    3 in [1, 2, 3]

s.py:
for x in range(1000000):
    3 in set([1, 2, 3])

~/py $ time python l.py

real    0m0.314s
user    0m0.275s
sys 0m0.030s

~/py $ time python s.py

real    0m1.055s
user    0m1.006s
sys 0m0.029s

【讨论】:

    【解决方案4】:

    为了将列表转换为集合,您需要遍历列表的元素,这最多需要O(n) 时间。我相信 Python 集是由哈希映射支持的,这意味着它们实际上对于查找操作也具有 O(n) 的最坏情况时间复杂度。

    他们的wiki 似乎同意这一点。

    【讨论】:

    • 集合查找的最坏情况复杂度实际上是非常难以达到的。
    • 确实如此。除非您创建一个只有一两个桶来保存密钥的哈希表,否则这将是愚蠢的。
    • CPython 的 dict 没有桶,它使用开放寻址。除此之外,即使有桶,你也不能从 Python 中影响它们的数量。您必须创建一个非常糟糕的散列函数,以至于每个键都具有相同的散列。没有任何内置类型,因此您必须竭尽全力创建具有如此糟糕的哈希函数的类型。
    • 很高兴知道(我对 CPython 内部结构不是很熟悉,抱歉)。但是,您仍然可以使用内置的哈希函数来碰巧发生冲突的值……如前所述,在正常情况下几乎不可能实现。
    【解决方案5】:

    因为将列表转换为集合需要遍历整个列表,这与测试列表是否包含值的复杂度相当。

    所以测试一个值是否在一个集合中会更快,只有这个集合已经被构造了。

    【讨论】:

    • “等价的复杂性”是一个红鲱鱼,因为各个步骤可能需要不同的时间。
    【解决方案6】:

    让我们试试吧……

    import cProfile
    

    我们选择了一个足够大的测试范围,以便我们可以实际测量一些东西。 2**13 只是一些随机值。

    test = range(2**13)
    runs = len(test)
    wcn  = runs - 1 # worst case n
    

    测试运行的数量等于列表中的数字数量,因此我们最终可以获得一个不错的平均值。 wcn 是最坏的情况,因为它是列表中的最后一个条目,所以它是算法将检查的最后一个条目。

    def simple():
        for n in range(runs):
            if n in test:
                pass
    
    def simpleWorstCase():
        for n in range(runs):
            if wcn in test:
                pass
    
    def slow():
        for n in range(runs):
            if n in set(test):
                pass
    

    我们简单测试的结果:

    cProfile.run('simple()')
    """
             4 function calls in 0.794 seconds
    
       Ordered by: standard name
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1    0.000    0.000    0.794    0.794 <string>:1(<module>)
            1    0.794    0.794    0.794    0.794 profile.py:6(simple)
            1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
            1    0.000    0.000    0.000    0.000 {range}
    """
    

    我们简单的最坏情况测试的结果:

    cProfile.run('simpleWorstCase()')
    """
             4 function calls in 1.462 seconds
    
       Ordered by: standard name
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1    0.000    0.000    1.462    1.462 <string>:1(<module>)
            1    1.462    1.462    1.462    1.462 profile.py:12(simpleWorstCase)
            1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
            1    0.000    0.000    0.000    0.000 {range}
    """
    

    我们首先转换为集合的测试结果:

    cProfile.run('slow()')
    """
             4 function calls in 2.227 seconds
    
       Ordered by: standard name
    
       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
            1    0.000    0.000    2.227    2.227 <string>:1(<module>)
            1    2.227    2.227    2.227    2.227 profile.py:11(slow)
            1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
            1    0.000    0.000    0.000    0.000 {range}
    """
    

    【讨论】:

    • 这显然是有缺陷的,您应该在进行任何收容检查之前创建set
    • 这个测试是关于在进行x in y检查之前测量转​​换为set的性能,所以我们必须在每次迭代中进行。它模拟在您的项目代码中到处执行此操作。
    猜你喜欢
    • 1970-01-01
    • 2016-03-08
    • 1970-01-01
    • 1970-01-01
    • 2019-02-09
    • 1970-01-01
    • 2016-11-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多