如何使 Python 程序快如闪电,提速 30%?

讨厌 Python 的人总是说,他们不想使用它的原因之一是它很慢。不管使用什么编程语言,程序是快还是慢都在很大程度上取决于编写程序的开发人员,以及他们编写最优化快速程序的技能和能力。在本文中,让我们来证明一下某些人的“误解”,看看如何提高 Python 程序的性能,使它们变得非常快!

计时和性能分析

在我们开始优化任何东西之前,我们首先需要找出到底是代码的哪些部分减慢了整个程序。有时候,程序的瓶颈可能是显而易见的,但如果你不知道它在哪里,那么以下选项可以帮你找出来。

这是我将用于演示的程序,它计算 e 的 X 次方

# slow_program.py

fromdecimalimport*

defexp(x):

getcontext().prec +=2

i, lasts, s, fact, num =0,0,1,1,1

whiles != 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.8slow_program.py

real0m11,058s

user0m11,050s

sys0m0,008s

如果你只是想计算整个程序的运行时间,这就行了,但这通常不能满足需求……

最详细的性能分析

另一个极端是 cProfile,它提供的信息又太多了:

~$python3.8-mcProfile-stimeslow_program.py

1297functioncalls(1272primitivecalls)in11.081seconds

Ordered by:internaltime

ncallstottimepercallcumtimepercallfilename:lineno(function)

311.0793.69311.0793.693slow_program.py:4(exp)

10.0000.0000.0020.002{built-inmethod_imp.create_dynamic}

4/10.0000.00011.08111.081{built-inmethodbuiltins.exec}

60.0000.0000.0000.000{built-inmethod__new__oftypeobjectat0x9d12c0}

60.0000.0000.0000.000abc.py:132(__new__)

230.0000.0000.0000.000_weakrefset.py:36(__init__)

2450.0000.0000.0000.000{built-inmethodbuiltins.getattr}

20.0000.0000.0000.000{built-inmethodmarshal.loads}

100.0000.0000.0000.000:1233(find_spec)

8/40.0000.0000.0000.000abc.py:196(__subclasscheck__)

150.0000.0000.0000.000{built-inmethodposix.stat}

60.0000.0000.0000.000{built-inmethodbuiltins.__build_class__}

10.0000.0000.0000.000__init__.py:357(namedtuple)

在这里,我们使用 cProfile 模块和 time 参数运行测试脚本,这样就可以根据内部时间(cumtime)对代码行进行排序。这给了我们很多信息,上面的内容大约是实际输出的 10%。从这里,我们可以看到 exp 函数是罪魁祸首(惊喜!),现在我们可以得到更具体的时间和性能分析…

对具体的函数计时

现在我们知道了应该将注意力放在哪里,我们可能希望对慢速函数进行计时,而不需要测量代码的其余部分。我们可以使用简单的装饰器:

deftimeit_wrapper(func):

@wraps(func)

defwrapper(*args, **kwargs):

start =time.perf_counter()# Alternatively, you can usetime.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)

)

returnfunc_return_val

returnwrapper

接下来,可以把这个装饰器应用到函数上,像下面这样:

@timeit_wrapper

defexp(x):

...

print('{0:<10} {1:<8} {2:^8}'.format('module','function','time'))

exp(Decimal(150))

exp(Decimal(400))

exp(Decimal(3000))

输出如下:

~ $python3.8slow_program.py

module functiontime

__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_cache 缓存数据

importfunctools

importtime

# 最多缓存 12 个不同的结果

@functools.lru_cache(maxsize=12)

defslow_func(x):

time.sleep(2)# 模拟长时间计算

returnx

slow_func(1)# ... 等待 2 秒才能获得结果

slow_func(1)# 结果已缓存,会立即返回

slow_func(3)# ... 等待 2 秒才能获得结果

上面的函数使用 time.sleep 模拟大量计算。第一次使用参数 1 调用时,它将等待 2 秒,然后才返回结果。当再次调用时,结果已经被缓存,因此,它会跳过函数体并立即返回结果。

使用局部变量

这与在每个作用域内查找变量的速度有关。我会写每个作用域,因为它不只关乎使用局部变量还是全局变量。查找速度也确实存在差异,函数中的局部变量最快,类级属性(例如 self.name)次之,而全局(例如导入的函数 time.time)变量最慢。

你可以像下面这样,使用不必要的赋值来提升性能:

# 示例#1

classFastClass:

defdo_stuff(self):temp =self.value# 这可以加速循环中的查找

foriinrange(10000):

...# 在这里使用`temp`做些操作

# 示例#2

import random

deffast_function():

r = random.random

foriinrange(10000):

print(r())# 在这里调用`r()`,比全局的 random.random() 要快

使用函数

这看起来可能不符合直觉,因为调用函数会将更多的东西放到堆栈中,从函数返回时会产生开销,但这与前面一点有关。如果你只是将整个代码放入一个文件中,而不将其放入函数中,那么由于全局变量的关系,速度会慢很多。因此,你只是将整个代码封装在 main 函数中并调用一次,就可以加快你的代码,像这样:

defmain():

...# 之前所有的全局代码

main()

不要访问属性

另一个可能降低程序速度的是点操作符(.),它可以用于访问对象属性。这个操作符使用 _getattribute__ 触发字典查找,这会在代码中产生额外的开销。那么,我们如何才能避免(限制)使用它呢?

# 慢:

importre

defslow_func():

foriinrange(10000):

re.findall(regex, line)# 慢!

# 快:

fromreimportfindall

deffast_func():

foriinrange(10000):

findall(regex, line)# 较快!

提防字符串

在循环中运行诸如模数(%s)或.format() 之类的方法时,对字符串的操作可能会变得非常慢。我们还有什么更好的选择吗?根据 Raymond Hettinger 最近的推文,我们唯一应该使用的是 f-string,它是最易读、最简洁、最快速的方法。因此,根据那条推文,你可以使用以下方法——从最快的到最慢的:

f'{s} {t}'# 快!

s +' '+ t

' '.join((s, t))

'%s %s'% (s, t)

'{} {}'.format(s, t)

Template('$s$t').substitute(s=s, t=t)# 慢!

我自己建了个群,对 JAVA 开发有兴趣的朋友欢迎加入QQ群:322708204进行技术讨论,里面资深架构师会分享一些整理好的BATJ面试题:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。

生成器本身并没有更快,因为它们允许延迟计算,这节省的是内存而不是时间。但是,节省的内存可能会使得程序在实际运行时更快。为什么?如果你有一个大型数据集,并且没有使用生成器(迭代器),那么数据可能会溢出 CPU L1 缓存,这将显著降低在内存中查找值的速度。

小结

优化的第一原则是不做优化。但是,如果你真的需要,我希望这些小技巧能帮到你。不过,在优化代码时要注意,因为它可能会使代码难于阅读、难于维护,甚至超过优化带来的好处。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 写在前面的话 代码中的# > 表示的是输出结果 输入 使用input()函数 用法 注意input函数输出的均是字...
    FlyingLittlePG阅读 2,729评论 0 8
  • 1. Python的hello-world: print ("Hello, Python!")、 完了 摇就完事儿...
    LunarShade阅读 1,366评论 0 0
  • 个人笔记,方便自己查阅使用 Py.LangSpec.Contents Refs Built-in Closure ...
    freenik阅读 67,677评论 0 5
  • 1.收集50个标题 1.深圳三岁女童幼儿园摔折手臂,家长索赔12万,园方称将积极协商。 2.被“穷养”的男孩,会毁...
    听歌在山上阅读 145评论 3 0
  • 本文所有引用均来自廖老师的网站。转载请说明出处。 1. 输出 有关输出需要注意的有以下几个小点: 单引号和双引号功...
    掠光者阅读 238评论 0 0