如何让你的Python程序快如闪电
讨厌Python的人总是说,他们不想使用它的原因之一是它太慢了。好吧,特定程序(无论使用何种编程语言)是快还是慢在很大程度上取决于编写它的开发人员以及他们编写优化和快速程序的技能和能力。
所以,让我们证明一些人是错的,让我们看看如何提高Python程序的性能并使它们变得非常快!
时序和分析
在我们开始优化任何东西之前,我们首先需要找出代码的哪些部分实际上减慢了整个程序。有时程序的瓶颈可能很明显,但如果你不知道它在哪里,那么你可以通过以下方式找出瓶颈:
注意:这是我将用于演示目的的程序,它的计算e能力为X(取自 Python 文档):
# slow_program.pyfrom 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 +sexp(Decimal(150))exp(Decimal(400))exp(Decimal(3000))
- 最懒惰的“剖析”
首先,最简单且非常懒惰的解决方案 - Unixtime命令:
$ time python3 slow_program.pyreal 0m14.762suser 0m14.634ssys 0m0.035s
- 最详细的分析
我们使用cProfile来分析一下,它会给你提供太多信息:
~ $ python3 -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 <frozen importlib._bootstrap_external>: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 <frozen importlib._bootstrap_external>:57(_path_join) 48 0.000 0.000 0.000 0.000 <frozen importlib._bootstrap_external>:59(<listcomp>) 1 0.000 0.000 11.081 11.081 slow_program.py:1(<module>)...
~cProfile在这里,我们运行带有模块和参数的测试脚本time,以便行按内部时间 ( cumtime) 排序。这给了我们很多信息,你在上面看到的行大约是实际输出的 10%。从这里我们可以看出exp功能是罪魁祸首,现在我们可以通过时间和分析获得更具体的信息......
- 时序特定功能
既然我们知道将注意力放在哪里,我们可能想要对慢速函数计时,而不测量其余代码。为此,我们可以使用简单的装饰器:
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
然后可以将此装饰器应用于被测函数,如下所示:
# slow_program.pyfrom decimal import *from functools import wrapsimport timedef 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@timeit_wrapperdef 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 +sprint('{0:<10} {1:<8} {2:^8}'.format('module', 'function', 'time'))exp(Decimal(150))exp(Decimal(400))exp(Decimal(3000))
需要考虑的一件事是我们实际(想要)测量什么样的时间。时间包提供time.perf_counter和time.process_time。这里的区别是perf_counter返回的是绝对值,其中包含了你的Python程序进程没有运行的时间,因此可能会受到机器负载的影响。另一方面process_time只返回用户时间(不包括系统时间),这只是你的进程的时间。
让它更快
现在是有趣的部分。让我们让你的 Python 程序运行得更快。我(主要)不会向你展示一些可以神奇地解决你的性能问题的黑客、技巧和代码片段。这更多的是关于一般的想法和策略,使用它们可以对性能产生巨大的影响,在某些情况下可以提高 30% 的速度。
- 使用内置数据类型
这个很明显。内置数据类型非常快,特别是与我们的自定义类型(如树或链表)相比。这主要是因为内置函数是在C中实现的,我们在用 Python 编码时无法真正匹配速度。
- 缓存/记忆化lru_cache
我已经在之前的文章中展示了这个,但我认为值得用简单的例子重复它:
import functoolsimport time# caching up to 12 different results@functools.lru_cache(maxsize=12)def slow_func(x): time.sleep(2) # Simulate long computation return xslow_func(1) # ... waiting for 2 sec before getting resultslow_func(1) # already cached - result returned instantaneously!slow_func(3) # ... waiting for 2 sec before getting result
上面的函数使用 模拟繁重的计算time.sleep。第一次使用参数调用时1,它等待 2 秒,然后才返回结果。再次调用时,结果已经被缓存,因此它会跳过函数体并立即返回结果。
- 使用局部变量
这与在每个范围内查找变量的速度有关。因为它不仅仅是关于使用局部变量还是全局变量。实际上,查找速度甚至在 - 比方说 - 函数中的局部变量(最快),类级属性(例如self.name- 较慢)和全局变量(例如导入函数time.time(最慢))之间存在差异。
你可以通过使用看似不必要(直接无用)的分配来提高性能,如下所示:
import randomdef fast_function(): r = random.random for i in range(10000): print(r()) # calling `r()` here, is fa ster than global random.random()fast_function()
- 使用函数
这似乎违反直觉,因为调用函数会将更多内容放入堆栈并从函数返回中产生开销,但这与前一点有关。如果你只是将整个代码放在一个文件中而不将其放入函数中,那么由于全局变量,它会慢得多。main因此,你可以通过将整个代码包装在函数中并调用一次来加速你的代码,如下所示:
def main(): ... # All your previously global codemain()
- 不要访问属性
另一个可能会降低程序速度的是点运算符( .),它在访问对象属性时使用。此运算符使用 触发字典查找__getattribute__,这会在你的代码中产生额外的开销。那么,我们如何才能真正避免(限制)使用它呢?
# Slow:import redef slow_func(): for i in range(10000): re.findall(regex, line) # Slow!# Fast:from re import findalldef fast_func(): for i in range(10000): findall(regex, line) # Faster!
- 当心字符串
使用例如模数( %s) 或循环运行时,对字符串的操作可能会变得非常慢.format()。我们有什么更好的选择?我们应该使用的是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!
- 生成器可以很快
生成器并不是天生就更快,因为它们是为了允许惰性计算而设计的,这样可以节省内存而不是时间。但是,节省的内存可能会使你的程序实际运行得更快。如何?好吧,如果你有大型数据集并且不使用生成器(迭代器),那么数据可能会溢出 CPU 的L1 缓存,这将显着减慢在内存中查找值的速度。
就性能而言,CPU 可以将其正在处理的所有数据尽可能靠近地保存在缓存中,这一点非常重要。
结论
优化的首要规则是不要这样做。但是,如果你真的必须这样做,那么我希望这些方法可以帮助你。
如果你发现我的任何文章对你有帮助或者有用,麻烦点赞、转发或者赞赏。 谢谢!