讨厌 Python 的人总是说,他们不想使用 Python 的原因之一是它的速度太慢。好吧,不管使用哪种编程语言,具体的程序是快还是慢,在很大程度上取决于编写程序的开发人员以及他们编写优化、快速程序的能力。
所以,让我们来证明那些人是错的——让我们看看如何提高 Python 程序的性能并使它们变得非常快!
时间和性能
在开始优化任何代码之前,我们首先需要找出代码的哪些部会减慢整个程序的速度。有时,程序的瓶颈可能很明显,但如果你不知道它在哪里,那么你可以从下面几个地方找到它:
注意:这是我用于演示的程序,它将 e 计算为 X 的幂(取自 Python 文档):
# slow_program.py
from decimal import *
def exp(x):
getcontext().prec += 2
i, lasts, s, fact, num = 0, 0, 1, 1, 1
while s != lasts:
lasts = s
i += 1
fact *= i
num *= x
s += num / fact
getcontext().prec -= 2
return +s
exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))
最懒的「剖析」
首先,最简单、最懒的解决方案——Unix time 命令:
~ $ time python3.8 slow_program.py
real 0m11,058s
user 0m11,050s
sys 0m0,008s
如果你只想给你的整个程序计时,这是可行的,但这通常是不够的。
最详细的分析
另一个是 cProfile,它会给你提供特别多的信息:
~ $ python3.8 -m cProfile -s time slow_program.py
1297 function calls (1272 primitive calls) in 11.081 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
3 11.079 3.693 11.079 3.693 slow_program.py:4(exp)
1 0.000 0.000 0.002 0.002 {built-in method _imp.create_dynamic}
4/1 0.000 0.000 11.081 11.081 {built-in method builtins.exec}
6 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x9d12c0}
6 0.000 0.000 0.000 0.000 abc.py:132(__new__)
23 0.000 0.000 0.000 0.000 _weakrefset.py:36(__init__)
245 0.000 0.000 0.000 0.000 {built-in method builtins.getattr}
2 0.000 0.000 0.000 0.000 {built-in method marshal.loads}
10 0.000 0.000 0.000 0.000 :1233(find_spec)
8/4 0.000 0.000 0.000 0.000 abc.py:196(__subclasscheck__)
15 0.000 0.000 0.000 0.000 {built-in method posix.stat}
6 0.000 0.000 0.000 0.000 {built-in method builtins.__build_class__}
1 0.000 0.000 0.000 0.000 __init__.py:357(namedtuple)
48 0.000 0.000 0.000 0.000 :57(_path_join)
48 0.000 0.000 0.000 0.000 :59()
1 0.000 0.000 11.081 11.081 slow_program.py:1()
...
在这里,我们使用 cProfile 模块和 time 参数运行测试脚本,以便按内部时间(cumtime)对行进行排序。这给了我们很多信息,你可以看到上面的行大约是实际输出的 10%。由此我们可以看出 exp 函数是罪魁祸首(是不是感到很惊奇?),现在我们可以更具体地了解时间和分析了!
特定函数计时
既然我们知道该将注意力集中在哪里,我们可能希望对慢函数进行计时,而不测量代码的其余部分。为此,我们可以使用简单的 decorator:
def timeit_wrapper(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter() # Alternatively, you can use time.process_time()
func_return_val = func(*args, **kwargs)
end = time.perf_counter()
print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__, end - start))
return func_return_val
return wrapper
这个 decorator 随后可以应用于测试中的函数,如下所示:
@timeit_wrapper
def exp(x):
...
print('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time'))
exp(Decimal(150))
exp(Decimal(400))
exp(Decimal(3000))
其输出如下:
~ $ python3.8 slow_program.py
module function time
__main__ .exp : 0.003267502994276583
__main__ .exp : 0.038535295985639095
__main__ .exp : 11.728486061969306
这里要考虑的一件事是,我们实际想要测量的是什么样的时间。时间包提供 time.perf_counter 和 time.process_time。这里的区别在于 perf_counter 返回绝对值,其中包括 Python 程序进程未运行的时间,因此它可能会受到机器负载的影响。另一方面,process_time 只返回用户时间(不包括系统时间),这只是进程的时间。
让程序跑得更快
现在,有趣的是。让我们让你的 Python 程序运行得更快。我基本上不会向你展示一些能够神奇地解决性能问题的技巧和代码片段。这更多的是关于一般的想法和策略,当你使用这些策略时,它们可以对性能产生巨大的影响,在某些情况下甚至可以提高 30% 的速度。
使用内置数据类型
这一点很明显。内置数据类型非常快,特别是与我们的自定义类型(如树或链列表)相比。这主要是因为内置代码是用 C 语言实现的,在用 Python 编写代码时,我们在速度上无法与之相比。
使用 lru 缓存的缓存/备忘录
我已经在之前的博文(https://martinheinz.dev/blog/4)中写过这个,但是我认为有必要用一个简单的例子来重复一下: