Python 调优策略
i. 理解程序
进行任何优化之前,需要知道通过优化程序的某个部分所获得的加速与该部分所占的执行时间直接相关。例如,如果优化一个函数,使其运行速度变成了原来的10倍,但该函数的执行时间仅占程序总时间的10%,那么将仅能获得9%~10%的总体加速。根据执行优化所涉及的工作,这种优化成果可能并不值得一提。
首先在要有优化的代码上使用探测模块是一种不错的做法。你其实只想关注占用程序大部分执行时间的函数和方法,而不是偶尔调用的次要操作。
a. 理解算法
即使是糟糕的O(nlog n)算法实现也会比经过最优化的O(n^3)算法性能更高。不要优化低效的算法,首先应查找更好的算法
b. 使用内置类型
Python 内置的元组、列表、集合和字典类型完全是用C实现的,而且是解释器中经过最好优化的数据结构。应该积极使用这些类型来存储和操作程序中的数据,尽量避免构建自定义数据结构(如二进制搜索树、链表等)来模仿他们的功能。
根据这一标准,还应该积极使用标准库中的类型。一些库模块提供的新类型在处理特定任务上比内置类型性能更高。例如,collection.deque 类型提供了与列表类似的功能,但针对在两端插入新项进行了高度优化。相反,列表只有在末尾附近附加项目时才具有较高的效率。如果在前端插入项,需要移动所有其他元素来腾出空间。执行这一操作所需的时间会随着列表的不断变大而增长。
ii. 不要添加层
任何时候像对象或函数添加额外的抽象或便利层,都会降低程序的运行速度。但是,也需要在可用性和性能之间进行权衡。例如,添加额外层的总体目标通常是为了简化编码,这是不错的目标。
为了技能型掩饰,我们根除一个简单的例子,设想一个程序使用 dict() 函数来创建具有字符串健的字典,如下所示:
1 | s = dict(name="GOOD", shares=100, price=20.10) |
程序员可能以这种方式创建字典来节省键入操作。但是,这种创建字典的替代方法运行速度非常慢,因为它添加了额外的函数调用。
1 | In [5]: timeit.timeit("s = dict(name='GOOD', shares=100, price=20.10)") |
如果程序在运行时创建了数百万个字典,那么你应该知道第二种方法更快。在绝大多数情况下,添加增强或更改现有Python对象工作方式的任何功能在运行速度上都比较慢。
iii. 了解如何基于字典构建类和实例
用户定义的类和实例时用字典构建的。因此,查找、设置或删除实例数据的速度几乎总是比直接在字典上执行这些操作更慢。如果要做的只是构建一个简单的数据结构来存储数据,字典可能是比定义一个类更有效的选择。
为了掩饰两者间的差异,这里给出了一个简单的类来表示所持有的股票:
1 | class Stock(object): |
如果比较实用该类与实用字典的性能,结果会非常有趣。首先,让我们比较一下创建实例的性能:
1 | "s = Stock('GOOD', 100, 23.12)", "from stock import Stock") timeit( |
接下来看一下执行简单计算的性能:
1 | "s.shares * s.price", "from stock import Stock; s = Stock('GOOD', 100, 23.12)") timeit( |
出现这种情况是因为,可以使用class定义新对象,但这并不是处理数据的唯一途径。元素和字典通常就够用了。使用它们会是程序运行更快并且占用更少的内存。
iv. 使用__slots__
如果程序创建了用户定义累的大量实例,可以考虑在类定义中使用 __slots__ 属性。例如:
1 | class Stock(object): |
__slots__ 又是被看作一种安全功能,因为它能限制属性名称的设置。但是,它更主要的用途是在性能优化。使用 __slots__ 的类不使用字典存储实例数据,而是用一种更高效的内部数据结构。所以,不仅实例使用的内存少得多,而且访问实例数据的效率要更高。在某些情况下,仅仅添加 __slots__ 而不进行其他任何更改就会是程序的运行速度显著提高。
但是,使用 __slots__ 时有一个地方值得注意。将该功能添加到类中可能会无故破坏其他代码。例如,众所周知,实例将他们的数据存储在可作为 __dict__ 属性访问的字典中。定义slots时,该属性还不存在,所以依赖 __dict__ 的任何代码都会失败。
v. 避免使用 (.) 运算符
使用 (.) 在对象上查找属性时,总是会涉及名称查找。例如,如果使用x.name
, 将在环境中查找变量名称 x,然后在x上查找属性name。对于用户定义对象,属性查找还可能涉及在实例字典、类字典和积累的字典中查找。
对于大量使用方法或模块查找的计算,最好首先将要执行的操作放在一个局部变量中,从而避免属性查找。例如,如果执行大量求平方根操作,使用 from math import sqrt
和 sqrt(x)
比键入 math.sqrt(x)
更快。
显然,不应该尝试在程序的所有位置消除属性查找,因为这会使代码非常难以理解。但是,对于注重性能的部分,这是一种有用的技术。
vi. 使用异常来处理不常见的情况
要避免错误,你可能需要向程序中添加额外的检查。例如:
1 | def parse_header(line): |
但是,还可以采用另一种方法来处理错误,那就是让程序生成异常并捕获它。例如:
1 | def parse_header(line): |
如果在格式正确的行上对两个版本的代码进行基准测试,会发现第二个代码的运行速度更快。为在正常情况下不抛出异常的代码设置try代码块,这通常比执行if语句更快。
vii. 避免对常见的情况使用异常
不要在代码中对常见情况进行一场处理。例如,假设一个程序执行了大量字典查找,但大部分查找操作都是在查找不存在的键。现在,可以考虑两种执行查找的方法:
1 | # 方法1:执行查找并捕获异常 |
在简单的性能测试中(假设没有找到所需的键),第二种方法的运行速度比第一种方法快数倍,而且,第二种方法的运行速度几乎是用 items.get(key) 的两倍,因为 in 运算符的执行速度比方法调用更快。
viii. 鼓励使用函数式编程和迭代
列表包含、生成器表达式、生成器、协程和闭包比大多数Python程序员想象的要更为高效。尤其对于数据处理而言,与手动迭代数据并执行类似操作的代码相比,列表包含和生成器表达式的运行速度要快得多。这些操作的运行速度也比使用map() 和 filter() 等函数的旧式 Python 代码快得多。使用生成器编写的代码不仅能够最快的运行,还能够搞笑的使用内存。
ix. 使用装饰器和元类
装饰器和元类用于修改函数和类。但是,由于它们在定义函数或类时进行操作,所以可以通过多种方式使用它们来改进性能,特别是程序拥有很多可以启动和禁用的可选功能时。
节选自 《Python参考手册(第四版)》- 调优与优化
本文作者 : hellflame
原文链接 : https://hellflame.github.io/2017/10/06/python-optimize/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!