惯用语 Python:文字上的 `in` 关键字

Idiomatic Python: `in` keyword on literal

在文字上使用 in 运算符时,该文字是列表、集合还是元组最符合习惯?

例如

for x in {'foo', 'bar', 'baz'}:
    doSomething(x)

...

if val in {1, 2, 3}:
    doSomethingElse(val)

我看不出该列表有任何好处,但元组的不变性意味着它可以 被有效的解释器提升或重用。在 if 的情况下,如果它被重用,就会有效率上的好处。

cpython 中哪个最地道,哪个最高效?

Python提供了一个反汇编器,所以你经常可以只检查字节码:

In [4]: def checktup():
   ...:     for _ in range(10):
   ...:         if val in (1, 2, 3):
   ...:             print("foo")
   ...:

In [5]: def checkset():
   ...:     for _ in range(10):
   ...:         if val in {1, 2, 3}:
   ...:             print("foo")
   ...:


In [6]: import dis

对于 tuple 文字:

In [7]: dis.dis(checktup)
  2           0 SETUP_LOOP              32 (to 34)
              2 LOAD_GLOBAL              0 (range)
              4 LOAD_CONST               1 (10)
              6 CALL_FUNCTION            1
              8 GET_ITER
        >>   10 FOR_ITER                20 (to 32)
             12 STORE_FAST               0 (_)

  3          14 LOAD_GLOBAL              1 (val)
             16 LOAD_CONST               6 ((1, 2, 3))
             18 COMPARE_OP               6 (in)
             20 POP_JUMP_IF_FALSE       10

  4          22 LOAD_GLOBAL              2 (print)
             24 LOAD_CONST               5 ('foo')
             26 CALL_FUNCTION            1
             28 POP_TOP
             30 JUMP_ABSOLUTE           10
        >>   32 POP_BLOCK
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

对于 set-文字:

In [8]: dis.dis(checkset)
  2           0 SETUP_LOOP              32 (to 34)
              2 LOAD_GLOBAL              0 (range)
              4 LOAD_CONST               1 (10)
              6 CALL_FUNCTION            1
              8 GET_ITER
        >>   10 FOR_ITER                20 (to 32)
             12 STORE_FAST               0 (_)

  3          14 LOAD_GLOBAL              1 (val)
             16 LOAD_CONST               6 (frozenset({1, 2, 3}))
             18 COMPARE_OP               6 (in)
             20 POP_JUMP_IF_FALSE       10

  4          22 LOAD_GLOBAL              2 (print)
             24 LOAD_CONST               5 ('foo')
             26 CALL_FUNCTION            1
             28 POP_TOP
             30 JUMP_ABSOLUTE           10
        >>   32 POP_BLOCK
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

您会注意到,在这两种情况下,函数都会 LOAD_CONST,即,两次都经过了优化。即使 更好 ,在 set 文字的情况下,编译器已经保存了一个 frozenset,在函数的构造过程中,窥孔优化器已经管理弄清楚可以成为 set 的不变等价物。

注意,在Python 2 上,编译器每次都构建一个集合!:

In [1]: import dis

In [2]: def checkset():
   ...:     for _ in range(10):
   ...:         if val in {1, 2, 3}:
   ...:             print("foo")
   ...:

In [3]: dis.dis(checkset)
  2           0 SETUP_LOOP              49 (to 52)
              3 LOAD_GLOBAL              0 (range)
              6 LOAD_CONST               1 (10)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                35 (to 51)
             16 STORE_FAST               0 (_)

  3          19 LOAD_GLOBAL              1 (val)
             22 LOAD_CONST               2 (1)
             25 LOAD_CONST               3 (2)
             28 LOAD_CONST               4 (3)
             31 BUILD_SET                3
             34 COMPARE_OP               6 (in)
             37 POP_JUMP_IF_FALSE       13

  4          40 LOAD_CONST               5 ('foo')
             43 PRINT_ITEM
             44 PRINT_NEWLINE
             45 JUMP_ABSOLUTE           13
             48 JUMP_ABSOLUTE           13
        >>   51 POP_BLOCK
        >>   52 LOAD_CONST               0 (None)
             55 RETURN_VALUE

IMO,基本上没有 "idiomatic" 问题中所示的文字值用法。这些值对我来说看起来像 "magic numbers"。对 "performance" 使用文字可能会被误导,因为它牺牲了可读性以获得边际收益。在性能确实很重要的情况下,使用文字不太可能有太大帮助,无论如何都有更好的选择。

我认为惯用的做法是将这些值存储在全局或 class 变量中,尤其是当您在多个地方使用它们时(但即使您没有使用)。这提供了一些关于值的用途的文档,并使其更容易更新。然后,您可以在 function/method 定义中记住这些值,以在必要时提高性能。

至于哪种类型的数据结构最合适,这取决于你的程序做什么以及它如何使用数据。例如,顺序重要吗?对于 if x in y,它不会,但也许您正在使用 forif 中的数据。没有上下文,很难说什么是最好的选择。

这是一个我认为可读、可扩展且高效的示例。在函数定义中记住全局 ITEMS 可以加快查找速度,因为 items 在函数的本地命名空间中。如果您查看反汇编代码,您会发现 items 是通过 LOAD_FAST 而不是 LOAD_GLOBAL 查找的。这种方法还避免了制作项目列表的多个副本,如果它足够大,这可能是相关的(尽管,如果它足够大,您可能无论如何都不会尝试将其内联)。就我个人而言,大多数时候 我不会为这些优化而烦恼,但它们在某些情况下很有用。

# In real code, this would have a domain-specific name instead of the
# generic `ITEMS`.
ITEMS = {'a', 'b', 'c'}

def filter_in_items(values, items=ITEMS):
    matching_items = []
    for value in values:
        if value in items:
            matching_items.append(value)
    return matching_items

def filter_not_in_items(values, items=ITEMS):
    non_matching_items = []
    for value in values:
        if value not in items:
            non_matching_items.append(value)
    return non_matching_items

print(filter_in_items(('a', 'x')))      # -> ['a']
print(filter_not_in_items(('a', 'x')))  # -> ['x']

import dis
dis.dis(filter_in_items)