在Python中轻松实现性能优化的方法

113 阅读3分钟

当你想到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 而不是== 来进行单子比较

当与单子对象进行比较时,像TrueFalseNoneis 应该比== 更加可取。这是因为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 的调用先于条件检查。因此,它最终会比第一个版本慢一些。