超干分享!如何提高 Python 的运行速度?

共 2767字,需浏览 6分钟

 ·

2020-12-08 14:05

点击上方 “AirPython”,选择 “加为星标
第一时间关注 Python 技术干货!


Python 已经得到了全球程序员的喜爱,但是还是遭到一些人的诟病,原因之一就是认为它运行缓慢。


其实某个特定程序(无论使用何种编程语言)的运行速度是快还是慢,在很大程度上取决于编写该程序的开发人员自身素质,以及他们编写优化而高效代码的能力。


Medium 上一位小哥就详细讲了讲如何让 Python 提速 30%,以此证明代码跑得慢不是 Python的问题,而是代码本身的问题。


01
时序分析



在开始进行任何优化之前,我们首先需要找出代码的哪些部分使整个程序变慢。有时程序的问题很明显,但是如果你一时不知道问题出在哪里,那么这里有一些可能的选项:


注意:这是我将用于演示的程序,它将进行指数计算


# 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 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函数是罪魁祸首,现在我们可以更详细地了解时序和性能分析。


时序特定功能


现在我们知道了应当主要关注哪里,我们可能想对运行速度缓慢的函数计时,而不用测量其余的代码。为此,我们可以使用一个简单的装饰器:


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


然后可以将此装饰器应用于待测功能,如下所示:


@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_countertime.process_time两个函数。他们的区别在于perf_counter返回的绝对值,包括你的Python程序进程未运行时的时间,因此它可能会受到计算机负载的影响。另一方面,process_time仅返回用户时间(不包括系统时间),这仅是你的过程时间。


02
加速吧!



让Python程序运行得更快,这部分会很有趣!我不会展示可以解决你的性能问题的技巧和代码,更多地是关于构想和策略的,这些构想和策略在使用时可能会对性能产生巨大影响,在某些情况下,可以将速度提高30%。


使用内置数据类型


这一点很明显。内置数据类型非常快,尤其是与我们的自定义类型(例如树或链接列表)相比。这主要是因为内置程序是用C实现的,因此在使用Python进行编码时我们的速度实在无法与之匹敌。


使用lru_cache缓存/记忆


我已经在上一篇博客中展示了此内容,但我认为值得用简单的示例来重复它:


import functools
import time
# caching up to 12 different results
@functools.lru_cache(maxsize=12)
def slow_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


上面的函数使用time.sleep模拟大量计算。第一次使用参数1调用时,它将等待2秒钟,然后才返回结果。再次调用时,结果已经被缓存,因此它将跳过函数的主体并立即返回结果。有关更多实际示例,请参见以前的博客文章。


使用局部变量


这与在每个作用域中查找变量的速度有关,因为它不只是使用局部变量还是全局变量。实际上,即使在函数的局部变量(最快),类级属性(例如self.name——较慢)和全局(例如,导入的函数)如time.time(最慢)之间,查找速度实际上也有所不同。


你可以通过使用看似不必要的分配来提高性能,如下所示:


# Example #1
class FastClass:
    def do_stuff(self):
        temp = self.value # this speeds up lookup in loop
        for i in range(10000):
            ... # Do something with `temp` here

# Example #2
import random
def fast_function():
    r = random.random
    for i in range(10000):
        print(r()) # calling `r()` here, is faster than global random.random()


使用函数


这似乎违反直觉,因为调用函数会将更多的东西放到堆栈上,并从函数返回中产生开销,但这与上一点有关。如果仅将整个代码放在一个文件中而不将其放入函数中,则由于全局变量,它的运行速度会慢得多。因此,你可以通过将整个代码包装在main函数中并调用一次来加速代码,如下所示:


def main():

    ... # All your previously global code

main()


不访问属性


可能会使你的程序变慢的另一件事是点运算符(.),它在获得对象属性时被使用。此运算符使用__getattribute__触发字典查找,这会在代码中产生额外的开销。那么,我们如何才能真正避免(限制)使用它呢?


# Slow:
import re
def slow_func():
    for i in range(10000):
        re.findall(regex, line) # Slow!

# Fast:
from re import findall
def fast_func():
    for i in range(10000):
        findall(regex, line) # Faster!


当心字符串


使用模数(%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!


生成器本质上并没有更快,因为它们被允许进行延迟计算,从而节省了内存而不是时间。但是,保存的内存可能会导致你的程序实际运行得更快。这是怎么做到的?如果你有一个很大的数据集,而没有使用生成器(迭代器),那么数据可能会溢出CPU L1缓存,这将大大减慢内存中值的查找速度。


在性能方面,非常重要的一点是CPU可以将正在处理的所有数据尽可能地保存在缓存中。你可以观看Raymond Hettingers的视频,他在其中提到了这些问题。

03
结论



优化的首要规则是不要优化。但是,如果确实需要,那么我希望上面这些技巧可以帮助你。但是,在优化代码时要小心,因为它可能最终使你的代码难以阅读,因此难以维护,这可能超过优化的好处。


原文链接:

https://towardsdatascience.com/making-python-programs-blazingly-fast-c1cd79bd1b32






推荐阅读


带你用 Python 实现自动化群控(入门篇)

这些自动化场景,批处理完全可以取代 Python

我用几行 Python 自动化脚本完美解决掉了小姐姐的微信焦虑感



得本文对你有帮助?请分享给更多人

浏览 46
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报