盘点提高 Python 代码效率的方法
第一招:蛇打七寸:定位瓶颈
首先,第一步是定位瓶颈。举个简单的栗子,一个函数可以从1秒优化到到0.9秒,另一个函数可以从1分钟优化到30秒,如果要花的代价相同,而且时间限制只能搞定一个,搞哪个?根据短板原理,当然选第二个啦。
一个有经验的程序员在这里一定会迟疑一下,等等?函数?这么说,还要考虑调用次数?如果第一个函数在整个程序中需要被调用100000次,第二个函数在整个程序中被调用1次,这个就不一定了。举这个栗子,是想说明,程序的瓶颈有的时候不一定一眼能看出来。还是上面那个选择,程序员的你应该有感觉的,大多数情况下:一个「可以」从一分钟优化到30秒的函数会比一个「可以」从1秒优化到0.9秒的函数更容易捕获我们的注意,因为有很大的进步空间嘛。
所以,这么多废话讲完,献上第一招,profile。这是python自带的定位程序瓶颈的利器!虽然它提供了三种选项profile,cProfile,hotshot。还分为内置和外置。但是,个人觉得一种足矣,外置cProfile。心法如下:
python-mprofile逗比程序.py
这招的效果会输出一系列东西,比如函数被调用了几次,总时间多少,其中有多少是这个函数的子函数花费的,每次花多少时间,等等。嘛一图胜千言:
filename:lineno(function):文件名:第几行(函数名)
ncalls:这货一共调用了几次
tottime:这货自己总共花了多少时间,也就是要除掉内部函数小弟们的花费
percall:平均每次调用花的时间,tottime除以ncalls
cumtime:这货还有它的所有内部函数小弟们的总花费
percall:跟上面那个percall差不多,不过是cumtime除以ncalls
找到最值得优化的点,然后干吧。
第二招:一蛇禅:只需一招
记得刚开始接触Python的时候,有一位学长告诉我,Python有一个牛逼的理想,它希望每一个用它的人能写出一模一样的程序。Python之禅有云:
Thereshouldbeone--andpreferablyonlyone--obviouswaytodoit
所以Python系专业的禅师提供了一些常用功能的onlyone的写法。本人看了一下传说中的PythonWiKi:PerformanceTips,总结了几个「不要酱紫」「要酱紫」。
合并字符串的时候不要酱紫:
s=""forsubstringinlist:s+=substring
要酱紫:
s="".join(slist)
格式化字符串的时候不要酱紫:
out="<html>"+head+prologue+query+tail+"</html>"
要酱紫:
out="<html>%s%s%s%s</html>"%(head,prologue,query,tail)
可以不用循环的时候就不要用循环,比如不要酱紫:
newlist=[]forwordinoldlist:newlist.append(word.upper())
要酱紫:
newlist=map(str.upper,oldlist)
或者酱紫:
newlist=[s.upper()forsinoldlist]
字典初始化,比较常用的:
wdict={}forwordinwords:ifwordnotinwdict:wdict[word]=0wdict[word]+=1
如果重复的word太多了的话,可以考虑用酱紫的模式来省掉大量判断:
wdict={}forwordinwords:try:wdict[word]+=1exceptKeyError:wdict[word]=1
尽量减少function调用次数,用内部循环代替,比如,不要酱紫:
x=0defdoit1(i):globalxx=x+ilist=range(100000)t=time.time()foriinlist:doit1(i)
要酱紫:
x=0defdoit2(list):globalxforiinlist:x=x+ilist=range(100000)t=time.time()doit2(list)
第三招:蛇之狙击:高速搜索
这一招部分来源于IBM:Python代码性能优化技巧,搜索算法的最高境界是O(1)的算法复杂度。也就是HashTable。本人幸本科的时候学了点数据结构。知道Python的list使用类似链表的方法实现的。如过列表很大的话,在茫茫多的项里面用ifXinlist_a来做搜索和判断效率是非常低的。
Python的tuple我用得非常少,不评论。另两个我用得非常多的是set和dict。这两个就是用的类似HashTable的实现方法。
所以尽量不要酱紫:
k=[10,20,30,40,50,60,70,80,90]foriinxrange(10000):ifiink:#Dosomethingcontinue
要酱紫:
```k=[10,20,30,40,50,60,70,80,90]k_dict={i:0foriink}
先把list转换成dictionary
foriinxrange(10000):ifiink_dict:#Dosomethingcontinue```
找list的交集,不要酱紫:
list_a=[1,2,3,4,5] list_b=[4,5,6,7,8] list_common=[aforainlist_aifainlist_b]
要酱紫:
list_a=[1,2,3,4,5] list_b=[4,5,6,7,8] list_common=set(list_a)&set(list_b)
第四招:小蛇蛇……:想不出来名字了,就是各种小Tips
变量交换不需要中间变量:a,b=b,a(这里有个神坑,至今记忆深刻:True,False=False,True)
如果使用Python2.x,用xrange代替range,如果用Python3.x,range已经是xrange了,xrange已经木有了。xrange不会像range一样生成一个列表,而是生成一个迭代器,省内存。
可以用x>y>z代替x>yandy>z。效率更高,可读性也更好。当然理论上x>y
add(x,y)一般会比a+b要快?这个本人有所怀疑,实验了一下,首先add不能直接用,要importoperator,第二,我的实验结果表示add(x,y)完全没有a+b快,更何况还要牺牲可读性。
while1确实比whileTrue要快那么一点点。做了两次实验,大概快了15%左右。
第五招:无蛇胜有蛇:代码之外的性能
代码之外嘛,除了硬件之外,就是编译器了,这里隆重推荐pypy。pypy是一种叫做just-in-time的即时编译器。这个编译器的特点就是编译一句跑一句,和静态的编译器的区别嘛,我在知乎上看到一个非常形象的比喻:
假定你是一个导演,静态编译就是让演员把整个剧本背下来吃透,然后连续表演一个小时。动态编译就是让演员表演两分钟,然后思考一下,再看一下剧本,再表演两分钟……
动态编译和静态编译各有所长,看你演的是电影还是话剧了。
此外还有一个Cython可以在python里内置一些C的代码。我用的非常少,但是关键时刻确实有效。