当你想到Python时,性能可能不是你脑海中浮现的第一件事。在一个典型的 I/O 密集型应用程序中也不需要它,在那里大部分的 CPU 周期都在等待。但是,如果一些小的修正可以给你的程序带来微小的性能提升,那也无妨,对吗?
这里有三个简单的修正,你可以给你的Python代码带来它应得的一点额外速度。
1.使用{} 而不是dict 来初始化一个 dictionary
当初始化一个新的字典时,使用{} 要比调用内置的dict 性能好得多。
$ python3 -m timeit "x = dict()"
2000000 loops, best of 5: 111 nsec per loop
$ python3 -m timeit "x = {}"
10000000 loops, best of 5: 30.7 nsec per loop
为了了解原因,让我们看一下这两个语句的字节码表示。
>>> dis.dis("x = dict()")
1 0 LOAD_NAME 0 (dict)
2 CALL_FUNCTION 0
4 STORE_NAME 1 (x)
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
>>> dis.dis("x = {}")
1 0 BUILD_MAP 0
2 STORE_NAME 0 (x)
4 LOAD_CONST 0 (None)
6 RETURN_VALUE
dict 因为它调用了一个本质上返回{} 的函数,所以比较慢。因此,任何出现的dict() 可以安全地替换为 {}。
然而,请注意,你不要在将被传递给许多函数的变量中使用{} (甚至是dict() ,因为它返回{} )。在这种情况下,你可能希望传递dict 可调用的变量,然后只在函数中执行该可调用的变量。
2.使用is 而不是== 来进行单子比较
当与单子对象进行比较时,像True 、False 和None ,is 应该比== 更加可取。这是因为is 直接比较两个对象的ID,而一个单子对象的ID在运行时永远不会改变。
$ python3 -m timeit "x = 1; x == None"
10000000 loops, best of 5: 32.3 nsec per loop
$ python3 -m timeit "x = 1; x is None"
10000000 loops, best of 5: 21.2 nsec per loop
然而,== 调用了可比性的self.__eq__ 方法。在上面的例子中,int 类的__eq__ 被调用。因此,尽管上例中的时间差异看起来不大,但对于具有更复杂的__eq__ 方法的类的实例来说,时间差异会增加。
3.避免不必要的调用len()
为了在一个条件下检查一个列表的长度是否为非零,这样做更具有性能。
让我们用一个稍有删减的片段来说明这两种变化。
$ python3 -m timeit "x = [1, 2, 3, 4, 5]; y = 5 if x else 6"
5000000 loops, best of 5: 66.9 nsec per loop
$ python3 -m timeit "x = [1, 2, 3, 4, 5]; y = 5 if len(x) else 6"
2000000 loops, best of 5: 109 nsec per loop
$ python3 -m timeit "x = [1, 2, 3, 4, 5]; y = 5 if bool(x) else 6"
2000000 loops, best of 5: 149 nsec per loop
这是因为bool(x) 最终会调用相当于len(x) 的方法,因为没有为list 定义__bool__ 方法。为了了解为什么第二段代码比第一段慢,让我们深入了解一下字节码。
>>> dis.dis("x = [1, 2, 3, 4, 5]; y = 5 if x else 6")
1 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 LOAD_CONST 2 (3)
6 LOAD_CONST 3 (4)
8 LOAD_CONST 4 (5)
10 BUILD_LIST 5
12 STORE_NAME 0 (x)
14 LOAD_NAME 0 (x)
16 POP_JUMP_IF_FALSE 22
18 LOAD_CONST 4 (5)
20 JUMP_FORWARD 2 (to 24)
>> 22 LOAD_CONST 5 (6)
>> 24 STORE_NAME 1 (y)
26 LOAD_CONST 6 (None)
28 RETURN_VALUE
>>> dis.dis("x = [1, 2, 3, 4, 5]; y = 5 if len(x) else 6")
1 0 LOAD_CONST 0 (1)
2 LOAD_CONST 1 (2)
4 LOAD_CONST 2 (3)
6 LOAD_CONST 3 (4)
8 LOAD_CONST 4 (5)
10 BUILD_LIST 5
12 STORE_NAME 0 (x)
14 LOAD_NAME 1 (len)
16 LOAD_NAME 0 (x)
18 CALL_FUNCTION 1
20 POP_JUMP_IF_FALSE 26
22 LOAD_CONST 4 (5)
24 JUMP_FORWARD 2 (to 28)
>> 26 LOAD_CONST 5 (6)
>> 28 STORE_NAME 2 (y)
30 LOAD_CONST 6 (None)
32 RETURN_VALUE
在第二种情况下,有4个额外的语句。语句POP_JUMP_IF_FALSE 返回列表的长度(如果你深入挖掘CPython的实现)。然而,在第二种情况下,对len 的调用先于条件检查。因此,它最终会比第一个版本慢一些。