搭上Python号小火箭,程序运行越来越快!

点击上方 "程序员小乐"关注, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约

每日英文

Some memories, are doomed to be unable to cancel, Is just like some people, is doomed to be unable to substitute is the same.

有些记忆,注定无法抹去;就好比有些人,注定无法替代一样。

每日掏心

正确地活着,每个阶段都会带给你更好的自己。成长不是单纯地为了脱单,而是让自己有力量去体验任何可能。

来自:读芯术 | 责编:乐乐

程序员小乐(ID:study_tech)第 765 次推文   图片来自 Unsplash

往日回顾:作为互联网人!春节回家如何优雅的表达自己的工作?

   正文   

Python是一个很酷的语言,因为你可以在很短的时间内利用很少的代码做很多事情。不仅如此,它还能轻松地支持多任务,比如多进程等。

但Python运行的慢是历来被诟病的,一些人黑Python的一点是Python的程序运行速度奇慢。

这一方面和语言有关,另一方面可能就是你代码的问题。其实,无论使用哪种编程语言,特定程序的运行速度很大程度上都取决于该程序的开发人员及其编写快而优的程序的技巧和能力。

 

语言方面的问题我们解决不了,所以只能在编程技巧上来提高程序的运行效率。

 

是时候证明给那些python黑粉,让他们看看如何提升Python程序性能并使其像坐上火箭一样运行飞快!

 

图源:Unsplash

 

时序分析

 

优化之前,首先要找到是哪部分代码拖慢了整个程序的运行。有时候程序的"瓶颈"不是很明显,如果找不到,以下是一些建议以供参考:

 

注意:这是一个计算e的x次幂的演示程序(出自Python文档):

 

# slow_program.pyfrom decimal import*defexp(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))

在GitHub上查看rawslow_program.py全部代码

最省力的“性能分析”

 

首先,最简单且最省力的解决方案是使用Unix的time命令:

 

~ $ time python3.8  slow_program.pyreal     0m11,058suser     0m11,050ssys      0m0,008s

在GitHub上查看rawbase_time.shell全部代码

如果只是给整个程序计时,它很有用,但还不足够……

 

最详细的性能分析

 

性能分析的另一方法是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 <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>)...

在GitHub上查看rawcprofile.shell全部代码

这里用cProfile模块和time参数运行测试脚本,以便按内部时间(cumtime)对行进行排序。从中可以得到很多信息,以上所列结果约为实际输出的10%。由此可见,exp函数就是拖慢程序的“罪魁祸首”(太神奇啦!),现在看看更详尽的时序和性能分析......

 

对特定函数计时

 

已经知道拖慢程序运行的函数,下一步可使用简单的修饰器,专门对该函数计时,不测量其余代码。如下所示:

 

deftimeit_wrapper(func):    @wraps(func)    defwrapper(*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

在GitHub上查看rawtimeit_decorator.py全部代码

该修饰器可以应用于功能测试,如下所示:

 

@timeit_wrapperdefexp(x):    ...print('{0:<10}{1:<8}{2:^8}'.format('module', 'function', 'time'))exp(Decimal(150))exp(Decimal(400))exp(Decimal(3000))

在GitHub上查看rawtimeit_decorator_usage.py全部代码

输出如下:

 

~ $ python3.8  slow_program.pymodule     function   time   __main__  .exp       :0.003267502994276583__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 functoolsimport time# caching up to 12  different results@functools.lru_cache(maxsize=12)defslow_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

在GitHub上查看rawlru_cache.py全部代码

以上函数使用time.sleep模拟大量运算。第一次使用参数1调用该函数时,返回结果需要2秒。再次调用时,结果已被缓存,因此会跳过函数主体并立即返回结果。更多内容请参见此处。

 

使用局部变量

 

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

 

可以通过运行无用的任务来提高性能,如下所示:

 

#  Example #1classFastClass:    defdo_stuff(self):        temp =self.value  # this speeds up lookup in loop        for i inrange(10000):            ...  # Do something with `temp` here#  Example #2import randomdeffast_function():    r = random.random    for i inrange(10000):        print(r())  # calling `r()` here, is faster than  global random.random()

在GitHub上查看rawlocal_vars.py全部代码

使用函数(Function)

 

这怎么和假想的不同?理论上调用函数不是会将更多的东西放到堆栈上,加大返回结果的负担吗?但实际上,使用函数确实能加快运行速度,这与前一点有关。将整个代码放在一个文件中而非函数中,它是全局变量而非局部变量,运行速度就会慢得多。因此,可以将整个代码包裹在main函数中并通过一次调用来加速代码,如下所示:

 

defmain():    ...  # All your previously global codemain()

在GitHub上查看rawglobal_vars.py全部代码

避免访问属性(Attribute)

 

可能拖慢程序的一个原因是使用点运算符(.)访问对象属性。该运算符通过使用__getattribute__方法触发了字典查找,使代码产生额外负担。那么,如何避免或减少属性访问?

 

#  Slow:import redefslow_func():    for i inrange(10000):        re.findall(regex, line)  # Slow!#  Fast:from re import findalldeffast_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

结语

 

优化的首要规则就是“不优化”。

 

若真的有必要优化,那我希望这些技巧会有所帮助。

 

但是,优化代码时一定要小心,因为优化的结果可能是代码难以阅读进而难以维护,这就得不偿失了。

 

最后,希望大家能搭上Python号火箭,编码越来越快!

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。

猜你还想看

阿里、腾讯、百度、华为、京东最新面试题汇集

了解Linux的基础知识和一般概念

高并发下的抽奖优化

这些方法,能够让你的Python程序快如闪电

关注「程序员小乐」,收看更多精彩内容

嘿,你在看吗

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值