在“if”子句中使用“in”时的元组或列表?

Posted

技术标签:

【中文标题】在“if”子句中使用“in”时的元组或列表?【英文标题】:Tuple or list when using 'in' in an 'if' clause? 【发布时间】:2014-10-11 15:55:27 【问题描述】:

哪种方法更好?使用元组,例如:

if number in (1, 2):

或列表,例如:

if number in [1, 2]:

推荐哪一种用于此类用途以及为什么(逻辑和性能方面)?

【问题讨论】:

第三个选项:set(会员测试更快)。 CPython 会做一些内部优化并将你的列表文字存储为一个元组...... 第四个选项:frozenset,它与设置的成员资格测试成本相同,O(1),但由于它是不可变的,python 解释器知道它需要分配的哈希表的确切大小,而不是而不是为其他元素留出空间。 @IceArdor:但仅在 Python 3 中;在 Python 2 中使用 set 字面量或 frozenset([...]) 表达式意味着必须首先创建对象,这比针对等长元组的成员资格测试成本更高。 @sapam:在这种情况下,简单的相等性测试将击败两者。您需要在此处考虑平均成本,而不是最佳情况。对于 2 个或更多元素,该集合获胜。前提是它是与字节码一起存储的常量。 【参考方案1】:

CPython 解释器将第二种形式替换为第一种形式

这是因为从常量加载元组是一个操作,但列表将是 3 个操作;加载两个整数内容并构建一个新的列表对象。

因为您使用的是其他方式无法访问的列表文字,所以它被替换为元组:

>>> import dis
>>> dis.dis(compile('number in [1, 2]', '<stdin>', 'eval'))
  1           0 LOAD_NAME                0 (number)
              3 LOAD_CONST               2 ((1, 2))
              6 COMPARE_OP               6 (in)
              9 RETURN_VALUE        

第二个字节码在 one 步骤中将 (1, 2) 元组作为常量加载。将此与创建未在成员资格测试中使用的列表对象进行比较:

>>> dis.dis(compile('[1, 2]', '<stdin>', 'eval'))
  1           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (2)
              6 BUILD_LIST               2
              9 RETURN_VALUE        

这里对于长度为 N 的列表对象需要 N+1 步。

此替换是针对 CPython 的窥视孔优化;见Python/peephole.c source。那么对于其他 Python 实现,您希望改用不可变对象。

也就是说,在使用 Python 3.2 及更高版本时,最佳选项是使用 set literal

if number in 1, 2:

因为窥视孔优化器将用 frozenset() 对象替换它,并且针对集合的成员资格测试是 O(1) 常量操作:

>>> dis.dis(compile('number in 1, 2', '<stdin>', 'eval'))
  1           0 LOAD_NAME                0 (number)
              3 LOAD_CONST               2 (frozenset(1, 2))
              6 COMPARE_OP               6 (in)
              9 RETURN_VALUE

此优化已添加到 Python 3.2,但并未向后移植到 Python 2。

因此,Python 2 优化器无法识别此选项,而且从内容构建 setfrozenset 的成本几乎可以保证比使用元组进行测试的成本更高。

集合成员资格测试是 O(1) 且快速;对元组进行测试是 O(n) 最坏的情况。尽管针对集合进行测试必须计算散列(更高的常量成本,为不可变类型缓存),但针对元组而不是第一个元素进行测试的成本总是会更高。所以平均而言,集合更容易更快:

>>> import timeit
>>> timeit.timeit('1 in (1, 3, 5)', number=10**7)  # best-case for tuples
0.21154764899984002
>>> timeit.timeit('8 in (1, 3, 5)', number=10**7)  # worst-case for tuples
0.5670104179880582
>>> timeit.timeit('1 in 1, 3, 5', number=10**7)  # average-case for sets
0.2663505630043801
>>> timeit.timeit('8 in 1, 3, 5', number=10**7)  # worst-case for sets
0.25939063701662235

【讨论】:

我听说有一些领先的 ​​Python 专家提供的 youtube 视频解释了 (C)Python 可以做的所有优化......:p @JonClements:一些,不是全部。 :-P @IceArdor:不,因为构建该集合的成本大于针对元组进行测试的成本。元组测试是 O(N) 最坏的情况,而创建集合保证花费 O(N) 加上成员资格测试。这不是关于文字语法,而是关于优化器识别你可以用frozenset常量替换集合。 很多真正关心效率的Python程序会预先构建正在匹配的数据结构、正则表达式等。因此,如果 ACCEPTABLE = 1,2 是预定义的(作为全局或 phantom kwarg),唯一的开销是测试,而不是测试对象的构造。 @endolith:设置文字仍然更好。我的回答中的 timeit 测试表明,对于 worst case,它们比使用元组更快,对于 best case,它们几乎是等价的。那是因为集合并不需要比较每个元素。

以上是关于在“if”子句中使用“in”时的元组或列表?的主要内容,如果未能解决你的问题,请参考以下文章

如何将`inSet`与列的元组一起使用?

在 SQL“IN”子句中使用元组

如何比较和搜索列表中的元素与列表 SML 中的元组

TypeError:列表索引必须是整数或切片,而不是尝试制作二维列表时的元组

Python MySQLDB:在列表中获取 fetchall 的结果

列表和元组