惯用语 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
,它不会,但也许您正在使用 for
和 if
中的数据。没有上下文,很难说什么是最好的选择。
这是一个我认为可读、可扩展且高效的示例。在函数定义中记住全局 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)
在文字上使用 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
,它不会,但也许您正在使用 for
和 if
中的数据。没有上下文,很难说什么是最好的选择。
这是一个我认为可读、可扩展且高效的示例。在函数定义中记住全局 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)