在'if'语句中使用'in'时选择元组还是列表?

37 投票
1 回答
2969 浏览
提问于 2025-04-18 17:41

哪种方法更好呢?是用元组,比如:

if number in (1, 2):

还是用列表,比如:

if number in [1, 2]:

在这种情况下,推荐使用哪种方式呢?为什么这样选择(从逻辑和性能的角度来看)?

1 个回答

55

CPython解释器会把第二种形式替换成第一种形式。

这是因为从常量中加载一个元组只需要一步操作,而加载一个列表则需要三步:加载两个整数的内容,然后再创建一个新的列表对象。

由于你使用的列表字面量在其他地方并没有被使用,所以它会被替换成一个元组:

>>> 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        

在这里,第二个字节码将一个(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+1步来处理一个长度为N的列表对象。

这种替换是CPython特有的一种小优化;你可以查看Python/peephole.c的源代码。对于其他的Python实现,建议使用不可变对象。

不过,从Python 3.2及以上版本来看,最佳选择是使用集合字面量

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

撰写回答