Python是一个很酷的语言,因为你可以在很短的时间内利用很少的代码做很多事情。不仅如此,它还能轻松地支持多任务,比如多进程等。
但Python运行的慢是历来被诟病的,一些人黑Python的一点是Python的程序运行速度奇慢。
这一方面和语言有关,另一方面可能就是你代码的问题。其实,无论使用哪种编程语言,特定程序的运行速度很大程度上都取决于该程序的开发人员及其编写快而优的程序的技巧和能力。
语言方面的问题我们解决不了,所以只能在编程技巧上来提高程序的运行效率。
是时候证明给那些python黑粉,让他们看看如何提升Python程序性能并使其像坐上火箭一样运行飞快!
图源:Unsplash
时序分析
优化之前,首先要找到是哪部分代码拖慢了整个程序的运行。有时候程序的"瓶颈"不是很明显,如果找不到,以下是一些建议以供参考:
注意:这是一个计算e的x次幂的演示程序(出自Python文档):
1. # slow_program.py
2. from decimal import*
3. defexp(x):
4. getcontext().prec +=2
5. i, lasts, s, fact, num =0, 0, 1, 1, 1
6. while s != lasts:
7. lasts = s
8. i +=1
9. fact *= i
10. num *= x
11. s += num / fact
12. getcontext().prec -=2
13. return+s
14. exp(Decimal(150))
15. exp(Decimal(400))
16. exp(Decimal(3000))
在GitHub上查看rawslow_program.py全部代码
最省力的“性能分析”
首先,最简单且最省力的解决方案是使用Unix的time命令:
- ~ $ time python3.8 slow_program.py
- real 0m11,058s
- user 0m11,050s
- sys 0m0,008s
在GitHub上查看rawbase_time.shell全部代码
如果只是给整个程序计时,它很有用,但还不足够……
最详细的性能分析
性能分析的另一方法是cProfile,从中能得到很大的信息量:
1. ~ $ python3.8 -m cProfile -s time slow_program.py
2. 1297 function calls (1272 primitive calls) in 11.081 seconds
3. Ordered by: internal time
4. ncalls tottime percall cumtime percall filename:lineno(function)
5. 3 11.079 3.693 11.079 3.693 slow_program.py:4(exp)
6. 1 0.000 0.000 0.002 0.002 {built-in method _imp.create_dynamic}
7. 4/1 0.000 0.000 11.081 11.081 {built-in method builtins.exec}
8. 6 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x9d12c0}
9. 6 0.000 0.000 0.000 0.000 abc.py:132(__new__)
10. 23 0.000 0.000 0.000 0.000 _weakrefset.py:36(__init__)
11. 245 0.000 0.000 0.000 0.000 {built-in method builtins.getattr}
12. 2 0.000 0.000 0.000 0.000 {built-in method marshal.loads}
13. 10 0.000 0.000 0.000 0.000 :1233(find_spec)
14. 8/4 0.000 0.000 0.000 0.000 abc.py:196(__subclasscheck__)
15. 15 0.000 0.000 0.000 0.000 {built-in method posix.stat}
16. 6 0.000 0.000 0.000 0.000 {built-in method builtins.__build_class__}
17. 1 0.000 0.000 0.000 0.000 __init__.py:357(namedtuple)
18. 48 0.000 0.000 0.000 0.000 :57(_path_join)
19. 48 0.000 0.000 0.000 0.000 :59()
20. 1 0.000 0.000 11.081 11.081 slow_program.py:1()
21. ...
在GitHub上查看rawcprofile.shell全部代码
这里用cProfile模块和time参数运行测试脚本,以便按内部时间(cumtime)对行进行排序。从中可以得到很多信息,以上所列结果约为实际输出的10%。由此可见,exp函数就是拖慢程序的“罪魁祸首”(太神奇啦!),现在看看更详尽的时序和性能分析......
对特定函数计时
已经知道拖慢程序运行的函数,下一步可使用简单的修饰器,专门对该函数计时,不测量其余代码。如下所示:
- deftimeit_wrapper(func):
- @wraps(func)
1. defwrapper(*args, **kwargs):
2. start = time.perf_counter() # Alternatively, you can use time.process_time()
3. func_return_val = func(*args, **kwargs)
4. end = time.perf_counter()
5. print('{0:<10}.{1:<8} : {2:<8}'.format(func.__module__, func.__name__, end - start))
6. return func_return_val
7. return wrapper
在GitHub上查看rawtimeit_decorator.py全部代码
该修饰器可以应用于功能测试,如下所示:
1. @timeit_wrapper
2. defexp(x):
3. ...
4. print('{0:<10}{1:<8}{2:^8}'.format('module', 'function', 'time'))
5. exp(Decimal(150))
6. exp(Decimal(400))
7. exp(Decimal(3000))
在GitHub上查看rawtimeit_decorator_usage.py全部代码
输出如下:
1. ~ $ python3.8 slow_program.py
2. module function time
3. __main__ .exp :0.003267502994276583
4. __main__ .exp :0.038535295985639095
- __main__ .exp : 11.728486061969306
在GitHub上查看rawrun_with_timeit_decorator.shell全部代码
要考虑的一个问题是实际/想要测量的时间类型是什么。Time程序包提供了time.perf_counter和time.process_time。两者的区别是:perf_counter返回绝对值,其中包括Python程序进程未运行时的时间,因此可能会受计算机负载的影响;而process_time仅返回用户时间(不包括系统时间),这仅是程序的运行时间。
加快程序运行速度
图源:Unsplash
这是全文有趣的部分,关于如何加快Python的程序运行速度。我并没有列出一些可以奇妙解决性能问题的小技巧或代码段,而是涉及一般性的构想和策略,它们能极大地提高性能,某些情况下甚至能将性能提高30%。
使用内置数据类型
显而易见,内置数据类型运行很快,尤其是与自定义类型(例如树或链表)相比。主要是因为内置程序是用C语言实现的,远超过用Python编码的运行速度。
使用lru_cache缓存/记忆
我已经在上一篇博文中讲过这块内容,但在此还是要用简单的示例说明:
- import functools
- import time
- # caching up to 12 different results
- @functools.lru_cache(maxsize=12)
- defslow_func(x):
- time.sleep(2) # Simulate long computation
- return x
- slow_func(1) # ... waiting for 2 sec before getting result
- slow_func(1) # already cached - result returned instantaneously!
- slow_func(3) # ... waiting for 2 sec before getting result
在GitHub上查看rawlru_cache.py全部代码
以上函数使用time.sleep模拟大量运算。第一次使用参数1调用该函数时,返回结果需要2秒。再次调用时,结果已被缓存,因此会跳过函数主体并立即返回结果。更多内容请参见此处。
使用局部变量
这与在每个作用域中查找变量的速度有关。我用了“每个作用域”这个字眼,因为它不仅仅是“使用局部变量还是全局变量”的问题。实际上,即使在函数的局部变量(最快)、类级属性(如self.name-较慢)和全局变量(如导入的函数,time.time-最慢)之间,查找速度也有所不同。
可以通过运行无用的任务来提高性能,如下所示:
1. # Example #1
2. classFastClass:
3. defdo_stuff(self):
4. temp =self.value # this speeds up lookup in loop
5. for i inrange(10000):
6. ... # Do something with `temp` here
7. # Example #2
8. import random
9. deffast_function():
10. r = random.random
11. for i inrange(10000):
12. print(r()) # calling `r()` here, is faster than global random.random()
在GitHub上查看rawlocal_vars.py全部代码
使用函数(Function)
这怎么和假想的不同?理论上调用函数不是会将更多的东西放到堆栈上,加大返回结果的负担吗?但实际上,使用函数确实能加快运行速度,这与前一点有关。将整个代码放在一个文件中而非函数中,它是全局变量而非局部变量,运行速度就会慢得多。因此,可以将整个代码包裹在main函数中并通过一次调用来加速代码,如下所示:
- defmain():
- ... # All your previously global code
- main()
在GitHub上查看rawglobal_vars.py全部代码
避免访问属性(Attribute)
可能拖慢程序的一个原因是使用点运算符(.)访问对象属性。该运算符通过使用__getattribute__方法触发了字典查找,使代码产生额外负担。那么,如何避免或减少属性访问?
- # Slow:
- import re
- defslow_func():
- for i inrange(10000):
- re.findall(regex, line) # Slow!
- # Fast:
- from re import findall
- deffast_func():
- for i inrange(10000):
- findall(regex, line) # Faster!
在GitHub上查看rawimports.py全部代码
当心使用字符串
在循环里使用格式符(%s)或.format()时,字符串操作可能会变得非常慢。有没有更好的选择?Raymond Hettinger在最近发布的推文中提到:唯一应该使用的是f-string(格式化字符串常量),它是最易读、最简洁且最快捷的方法。根据这篇推文,下面列出了可用的方法(由快到慢):
- f'{s}{t}' # Fast!
- s +' '+ t
- ' '.join((s, t))
- '%s %s'% (s, t)
- '{} {}'.format(s, t)
- Template('$s $t').substitute(s=s, t=t) # Slow!
在GitHub上查看rawstrings.py全部代码
本质上,生成器并没有变得更快,因为它在设计上允许延迟计算以节省内存而非节约时间。然而节省的内存也可以加快程序实际运行速度。怎么做?如果有一个很大的数据集且不使用生成器(迭代器),那么数据可能会溢出CPU的L1 cache(1级缓存),这将大大减慢内存的查找速度。
在性能方面,极重要的一点是:CPU可以将正在处理的所有数据尽可能地保存在缓存中。
图源:Unsplash