「干货」让Python性能起飞的15个技巧,你知道几个呢?

Python014

「干货」让Python性能起飞的15个技巧,你知道几个呢?,第1张

前言

Python 一直以来被大家所诟病的一点就是执行速度慢,但不可否认的是 Python 依然是我们学习和工作中的一大利器。本文总结了15个tips有助于提升 Python 执行速度、优化性能

关于 Python 如何精确地测量程序的执行时间,这个问题看起来简单其实很复杂,因为程序的执行时间受到很多因素的影响,例如操作系统、Python 版本以及相关硬件(CPU 性能、内存读写速度)等。在同一台电脑上运行相同版本的语言时,上述因素就是确定的了,但是程序的睡眠时间依然是变化的,且电脑上正在运行的其他程序也会对实验有干扰,因此严格来说这就是实验不可重复。

我了解到的关于计时比较有代表性的两个库就是 time 和 timeit 。

其中, time 库中有 time() 、 perf_counter() 以及 process_time() 三个函数可用来计时(以秒为单位),加后缀 _ns 表示以纳秒计时(自 Python3.7 始)。在此之前还有 clock() 函数,但是在 Python3.3 之后被移除了。上述三者的区别如下:

与 time 库相比, timeit 有两个优点:

timeit.timeit(stmt='pass', setup='pass', timer= , number=1000000, globals=None)参数说明:

本文所有的计时均采用 timeit 方法,且采用默认的执行次数一百万次。

为什么要执行一百万次呢?因为我们的测试程序很短,如果不执行这么多次的话,根本看不出差距。

Exp1:将字符串数组中的小写字母转为大写字母。

测试数组为 oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']。

方法一

方法二

方法一耗时 0.5267724000000005s ,方法二耗时 0.41462569999999843s ,性能提升 21.29%

Exp2:求两个 list 的交集。

测试数组:a = [1,2,3,4,5],b = [2,4,6,8,10]。

方法一

方法二

方法一耗时 0.9507264000000006s ,方法二耗时 0.6148200999999993s ,性能提升 35.33%

关于 set() 的语法: | 、 & 、 - 分别表示求并集、交集、差集。

我们可以通过多种方式对序列进行排序,但其实自己编写排序算法的方法有些得不偿失。因为内置的 sort() 或 sorted() 方法已经足够优秀了,且利用参数 key 可以实现不同的功能,非常灵活。二者的区别是 sort() 方法仅被定义在 list 中,而 sorted() 是全局方法对所有的可迭代序列都有效。

Exp3:分别使用快排和 sort() 方法对同一列表排序。

测试数组:lists = [2,1,4,3,0]。

方法一

方法二

方法一耗时 2.4796975000000003s ,方法二耗时 0.05551999999999424s ,性能提升 97.76%

顺带一提, sorted() 方法耗时 0.1339823999987857s

可以看出, sort() 作为 list 专属的排序方法还是很强的, sorted() 虽然比前者慢一点,但是胜在它“不挑食”,它对所有的可迭代序列都有效。

扩展 :如何定义 sort() 或 sorted() 方法的 key

1.通过 lambda 定义

2.通过 operator 定义

operator 的 itemgetter() 适用于普通数组排序, attrgetter() 适用于对象数组排序

3.通过 cmp_to_key() 定义,最为灵活

Exp4:统计字符串中每个字符出现的次数。

测试数组:sentence='life is short, i choose python'。

方法一

方法二

方法一耗时 2.8105250000000055s ,方法二耗时 1.6317423000000062s ,性能提升 41.94%

列表推导(list comprehension)短小精悍。在小代码片段中,可能没有太大的区别。但是在大型开发中,它可以节省一些时间。

Exp5:对列表中的奇数求平方,偶数不变。

测试数组:oldlist = range(10)。

方法一

方法二

方法一耗时 1.5342976000000021s ,方法二耗时 1.4181957999999923s ,性能提升 7.57%

大多数人都习惯使用 + 来连接字符串。但其实,这种方法非常低效。因为, + 操作在每一步中都会创建一个新字符串并复制旧字符串。更好的方法是用 join() 来连接字符串。关于字符串的其他操作,也尽量使用内置函数,如 isalpha() 、 isdigit() 、 startswith() 、 endswith() 等。

Exp6:将字符串列表中的元素连接起来。

测试数组:oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']。

方法一

方法二

方法一耗时 0.27489080000000854s ,方法二耗时 0.08166570000000206s ,性能提升 70.29%

join 还有一个非常舒服的点,就是它可以指定连接的分隔符,举个例子

life//is//short//i//choose//python

Exp6:交换x,y的值。

测试数据:x, y = 100, 200。

方法一

方法二

方法一耗时 0.027853900000010867s ,方法二耗时 0.02398730000000171s ,性能提升 13.88%

在不知道确切的循环次数时,常规方法是使用 while True 进行无限循环,在代码块中判断是否满足循环终止条件。虽然这样做没有任何问题,但 while 1 的执行速度比 while True 更快。因为它是一种数值转换,可以更快地生成输出。

Exp8:分别用 while 1 和 while True 循环 100 次。

方法一

方法二

方法一耗时 3.679268300000004s ,方法二耗时 3.607847499999991s ,性能提升 1.94%

将文件存储在高速缓存中有助于快速恢复功能。Python 支持装饰器缓存,该缓存在内存中维护特定类型的缓存,以实现最佳软件驱动速度。我们使用 lru_cache 装饰器来为斐波那契函数提供缓存功能,在使用 fibonacci 递归函数时,存在大量的重复计算,例如 fibonacci(1) 、 fibonacci(2) 就运行了很多次。而在使用了 lru_cache 后,所有的重复计算只会执行一次,从而大大提高程序的执行效率。

Exp9:求斐波那契数列。

测试数据:fibonacci(7)。

方法一

方法二

方法一耗时 3.955014900000009s ,方法二耗时 0.05077979999998661s ,性能提升 98.72%

注意事项:

我被执行了(执行了两次 demo(1, 2) ,却只输出一次)

functools.lru_cache(maxsize=128, typed=False) 的两个可选参数:

点运算符( . )用来访问对象的属性或方法,这会引起程序使用 __getattribute__() 和 __getattr__() 进行字典查找,从而带来不必要的开销。尤其注意,在循环当中,更要减少点运算符的使用,应该将它移到循环外处理。

这启发我们应该尽量使用 from ... import ... 这种方式来导包,而不是在需要使用某方法时通过点运算符来获取。其实不光是点运算符,其他很多不必要的运算我们都尽量移到循环外处理。

Exp10:将字符串数组中的小写字母转为大写字母。

测试数组为 oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']。

方法一

方法二

方法一耗时 0.7235491999999795s ,方法二耗时 0.5475435999999831s ,性能提升 24.33%

当我们知道具体要循环多少次时,使用 for 循环比使用 while 循环更好。

Exp12:使用 for 和 while 分别循环 100 次。

方法一

方法二

方法一耗时 3.894683299999997s ,方法二耗时 1.0198077999999953s ,性能提升 73.82%

Numba 可以将 Python 函数编译码为机器码执行,大大提高代码执行速度,甚至可以接近 C 或 FORTRAN 的速度。它能和 Numpy 配合使用,在 for 循环中或存在大量计算时能显著地提高执行效率。

Exp12:求从 1 加到 100 的和。

方法一

方法二

方法一耗时 3.7199997000000167s ,方法二耗时 0.23769430000001535s ,性能提升 93.61%

矢量化是 NumPy 中的一种强大功能,可以将操作表达为在整个数组上而不是在各个元素上发生。这种用数组表达式替换显式循环的做法通常称为矢量化。

在 Python 中循环数组或任何数据结构时,会涉及很多开销。NumPy 中的向量化操作将内部循环委托给高度优化的 C 和 Fortran 函数,从而使 Python 代码更加快速。

Exp13:两个长度相同的序列逐元素相乘。

测试数组:a = [1,2,3,4,5], b = [2,4,6,8,10]

方法一

方法二

方法一耗时 0.6706845000000214s ,方法二耗时 0.3070132000000001s ,性能提升 54.22%

若要检查列表中是否包含某成员,通常使用 in 关键字更快。

Exp14:检查列表中是否包含某成员。

测试数组:lists = ['life', 'is', 'short', 'i', 'choose', 'python']

方法一

方法二

方法一耗时 0.16038449999999216s ,方法二耗时 0.04139250000000061s ,性能提升 74.19%

itertools 是用来操作迭代器的一个模块,其函数主要可以分为三类:无限迭代器、有限迭代器、组合迭代器。

Exp15:返回列表的全排列。

测试数组:["Alice", "Bob", "Carol"]

方法一

方法二

方法一耗时 3.867292899999484s ,方法二耗时 0.3875405000007959s ,性能提升 89.98%

根据上面的测试数据,我绘制了下面这张实验结果图,可以更加直观的看出不同方法带来的性能差异。

从图中可以看出,大部分的技巧所带来的性能增幅还是比较可观的,但也有少部分技巧的增幅较小(例如编号5、7、8,其中,第 8 条的两种方法几乎没有差异)。

总结下来,我觉得其实就是下面这两条原则:

内置库函数由专业的开发人员编写并经过了多次测试,很多库函数的底层是用 C 语言开发的。因此,这些函数总体来说是非常高效的(比如 sort() 、 join() 等),自己编写的方法很难超越它们,还不如省省功夫,不要重复造轮子了,何况你造的轮子可能更差。所以,如果函数库中已经存在该函数,就直接拿来用。

有很多优秀的第三方库,它们的底层可能是用 C 和 Fortran 来实现的,像这样的库用起来绝对不会吃亏,比如前文提到的 Numpy 和 Numba,它们带来的提升都是非常惊人的。类似这样的库还有很多,比如Cython、PyPy等,这里我只是抛砖引玉。

原文链接:https://www.jb51.net/article/238190.htm

窍门一:关键代码使用外部功能包

Python简化了许多编程任务,但是对于一些时间敏感的任务,它的表现经常不尽人意。使用C/C++或机器语言的外部功能包处理时间敏感任务,可以有效提高应用的运行效率。这些功能包往往依附于特定的平台,因此你要根据自己所用的平台选择合适的功能包。简而言之,这个窍门要你牺牲应用的可移植性以换取只有通过对底层主机的直接编程才能获得的运行效率。以下是一些你可以选择用来提升效率的功能包:

Cython

Pylnlne

PyPy

Pyrex

这些功能包的用处各有不同。比如说,使用C语言的数据类型,可以使涉及内存操作的任务更高效或者更直观。Pyrex就能帮助Python延展出这样的功能。Pylnline能使你在Python应用中直接使用C代码。内联代码是独立编译的,但是它把所有编译文件都保存在某处,并能充分利用C语言提供的高效率。

窍门二:在排序时使用键

Python含有许多古老的排序规则,这些规则在你创建定制的排序方法时会占用很多时间,而这些排序方法运行时也会拖延程序实际的运行速度。最佳的排序方法其实是尽可能多地使用键和内置的sort()方法。譬如,拿下面的代码来说:

import operator

somelist = [(1, 5, , (6, 2, 4), (9, 7, 5)]

somelist.sort(key=operator.itemgetter(0))

somelist

#Output = [(1, 5, , (6, 2, 4), (9, 7, 5)]

somelist.sort(key=operator.itemgetter(1))

somelist

#Output = [(6, 2, 4), (1, 5, , (9, 7, 5)]

somelist.sort(key=operator.itemgetter(2))

somelist

#Output = [(6, 2, 4), (9, 7, 5), (1, 5, ]

在每段例子里,list都是根据你选择的用作关键参数的索引进行排序的。这个方法不仅对数值类型有效,还同样适用于字符串类型。

窍门三:针对循环的优化

每一种编程语言都强调最优化的循环方案。当使用Python时,你可以借助丰富的技巧让循环程序跑得更快。然而,开发者们经常遗忘的一个技巧是:尽量避免在循环中访问变量的属性。譬如,拿下面的代码来说:

lowerlist = ['this', 'is', 'lowercase']

upper = str.upper

upperlist = []

append = upperlist.append

for word in lowerlist:

append(upper(word))

print(upperlist)

#Output = ['THIS', 'IS', 'LOWERCASE']

每次你调用str.upper, Python都会计算这个式子的值。然而,如果你把这个求值赋值给一个变量,那么求值的结果就能提前知道,Python程序就能运行得更快。因此,关键就是尽可能减小Python在循环中的工作量。因为Python解释执行的特性,在上面的例子中会大大减慢它的速度。

(注意:优化循环的方法还有很多,这只是其中之一。比如,很多程序员会认为,列表推导式是提高循环速度的最佳方法。关键在于,优化循环方案是提高应用程序运行速度的上佳选择。)

窍门四:使用较新的Python版本

如果你在网上搜索Python,你会发现数不尽的信息都是关于如何升级Python版本。通常,每个版本的Python都会包含优化内容,使其运行速度优于之前的版本。但是,限制因素在于,你最喜欢的函数库有没有同步更新支持新的Python版本。与其争论函数库是否应该更新,关键在于新的Python版本是否足够高效来支持这一更新。

你要保证自己的代码在新版本里还能运行。你需要使用新的函数库才能体验新的Python版本,然后你需要在做出关键性的改动时检查自己的应用。只有当你完成必要的修正之后,你才能体会新版本的不同。

然而,如果你只是确保自己的应用在新版本中可以运行,你很可能会错过新版本提供的新特性。一旦你决定更新,请分析你的应用在新版本下的表现,并检查可能出问题的部分,然后优先针对这些部分应用新版本的特性。只有这样,用户才能在更新之初就觉察到应用性能的改观。

窍门五:尝试多种编码方法

每次创建应用时都使用同一种编码方法几乎无一例外会导致应用的运行效率不尽人意。可以在程序分析时尝试一些试验性的办法。譬如说,在处理字典中的数据项时,你既可以使用安全的方法,先确保数据项已经存在再进行更新,也可以直接对数据项进行更新,把不存在的数据项作为特例分开处理。请看下面第一段代码:

n = 16

myDict = {}

for i in range(0, n):

char = 'abcd'[i%4]

if char not in myDict:

myDict[char] = 0

myDict[char] += 1

print(myDict)

当一开始myDict为空时,这段代码会跑得比较快。然而,通常情况下,myDict填满了数据,至少填有大部分数据,这时换另一种方法会更有效率。

n = 16

myDict = {}

for i in range(0, n):

char = 'abcd'[i%4]

try:

myDict[char] += 1

except KeyError:

myDict[char] = 1

print(myDict)

在两种方法中输出结果都是一样的。区别在于输出是如何获得的。跳出常规的思维模式,创建新的编程技巧能使你的应用更有效率。

窍门六:交叉编译你的应用

开发者有时会忘记计算机其实并不理解用来创建现代应用程序的编程语言。计算机理解的是机器语言。为了运行你的应用,你借助一个应用将你所编的人类可读的代码转换成机器可读的代码。有时,你用一种诸如Python这样的语言编写应用,再以C++这样的语言运行你的应用,这在运行的角度来说,是可行的。关键在于,你想你的应用完成什么事情,而你的主机系统能提供什么样的资源。

Nuitka是一款有趣的交叉编译器,能将你的Python代码转化成C++代码。这样,你就可以在native模式下执行自己的应用,而无需依赖于解释器程序。你会发现自己的应用运行效率有了较大的提高,但是这会因平台和任务的差异而有所不同。

(注意:Nuitka现在还处在测试阶段,所以在实际应用中请多加注意。实际上,当下最好还是把它用于实验。此外,关于交叉编译是否为提高运行效率的最佳方法还存在讨论的空间。开发者已经使用交叉编译多年,用来提高应用的速度。记住,每一种解决办法都有利有弊,在把它用于生产环境之前请仔细权衡。)

在使用交叉编译器时,记得确保它支持你所用的Python版本。Nuitka支持Python2.6, 2.7, 3.2和3.3。为了让解决方案生效,你需要一个Python解释器和一个C++编译器。Nuitka支持许多C++编译器,其中包括Microsoft Visual Studio,MinGW 和 Clang/LLVM。

交叉编译可能造成一些严重问题。比如,在使用Nuitka时,你会发现即便是一个小程序也会消耗巨大的驱动空间。因为Nuitka借助一系列的动态链接库(DDLs)来执行Python的功能。因此,如果你用的是一个资源很有限的系统,这种方法或许不太可行。

第一个办法,降低数据集的大小。python处理数据,如果数据结构中的数据超过2GB,通常都会很慢。如何降低数据集大小,需要修改算法。

第二个办法,将数据结构采用数组array或者是numarray存贮。这样内存数量与查找效率都会提高。尽量不要使用大的dict。使用一个省内存的blist代替list

第三个办法,将数据通过共享内存,让C++扩展模块来处理。

常用的是第二种办法。就是换个数据结构就可以提高效率。