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

lin*_*ndy 25 python optimization tuples list python-internals

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

if number in (1, 2):
Run Code Online (Sandbox Code Playgroud)

或列表,如:

if number in [1, 2]:
Run Code Online (Sandbox Code Playgroud)

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

Mar*_*ers 37

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        
Run Code Online (Sandbox Code Playgroud)

这里第二个字节码(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        
Run Code Online (Sandbox Code Playgroud)

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

这种替换是CPython特有的窥视孔优化; 看到Python/peephole.c来源.对于其他 Python实现,您希望坚持使用不可变对象.

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

if number in {1, 2}:
Run Code Online (Sandbox Code Playgroud)

因为窥视孔优化器将用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
Run Code Online (Sandbox Code Playgroud)

这个优化是在Python 3.2中添加的,但没有向后移植到Python 2.

因此,Python 2优化器无法识别此选项,并且构建一个setfrozenset来自内容的成本几乎可以保证比使用元组进行测试更昂贵.

设置成员资格测试是O(1)和快速; 针对元组的测试是O(n)最坏的情况.虽然再次测试集合必须计算散列(更高的常量成本,缓存为不可变的tupes),但对于除第一个元素之外的元组进行测试的成本总是会更高.所以平均来说,集合很快就会更快:

>>> 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
Run Code Online (Sandbox Code Playgroud)

  • @IceArdor:不,因为构建该组的**成本**大于对元组进行测试的成本.元组测试是O(N)最坏的情况,而创建集合保证花费O(N),加上成员资格测试.这不是关于字面语法,这是关于优化器*识别*你可以用`frozenset`常量替换该集合. (6认同)
  • 许多真正关心效率的Python程序会预先构建正在匹配的数据结构、正则表达式等。因此,如果“ACCEPTABLE = {1,2}”是预定义的(作为全局或幻像 kwarg),则唯一的开销是测试,而不是构建要测试的内容。 (2认同)
  • @endolith:设置文字仍然更好。我的答案中的 timeit 测试表明,对于“最坏情况”,它们比使用元组更快,对于“最好情况”,它们几乎是等效的。这是因为集合非常不需要比较每个元素。 (2认同)