python编码最佳实践之总结
相信用python的同学不少,本人也一直对python情有独钟,毫无疑问python作为一门解释性动态语言没有那些编译型语言高效,但是python简洁、易读以及可扩展性等特性使得它大受青睐。
工作中很多同事都在用python,但往往很少有人关注它的性能和惯用法,一般都是现学现用,毕竟python不是我们的主要语言,我们一般只是使用它来做一些系统管理的工作。但是我们为什么不做的更好呢?pythonzen中有这样一句:Thereshouldbeone--andpreferablyonlyone--obviouswaytodoit.Althoughthatwaymaynotbeobviousatfirstunlessyou'reDutch.大意就是python鼓励使用一种最优的方法去完成一件事,这也是和ruby等的一个差异。所以一种好的python编写习惯个人认为很重要,本文就重点从性能角度出发对python的一些惯用法做一个简单总结,希望对大家有用~
提到性能,最容易想到的是降低复杂度,一般可以通过测量代码回路复杂度(cyclomaticcomplexitly)和Landau符号(大O)来分析,比如dict查找是O(1),而列表的查找却是O(n),显然数据的存储方式选择会直接影响算法的复杂度。
一、数据结构的选择
1.在列表中查找:
对于已经排序的列表考虑用bisect模块来实现查找元素,该模块将使用二分查找实现
deffind(seq,el): pos=bisect(seq,el) ifpos==0or(pos==len(seq)andseq[-1]!=el): return-1 returnpos-1
而快速插入一个元素可以用:
bisect.insort(list,element)
这样就插入元素并且不需要再次调用sort()来保序,要知道对于长list代价很高.
2.set代替列表:
比如要对一个list进行去重,最容易想到的实现:
seq=['a','a','b'] res=[] foriinseq: ifinotinres: res.append(i)
显然上面的实现的复杂度是O(n2),若改成:
seq=['a','a','b'] res=set(seq)
复杂度马上降为O(n),当然这里假定set可以满足后续使用。
另外,set的union,intersection,difference等操作要比列表的迭代快的多,因此如果涉及到求列表交集,并集或者差集等问题可以转换为set来进行,平时使用的时候多注意下,特别当列表比较大的时候,性能的影响就更大。
3.使用python的collections模块替代内建容器类型:
collections有三种类型:
deque:增强功能的类似list类型
defaultdict:类似dict类型
namedtuple:类似tuple类型
列表是基于数组实现的,而deque是基于双链表的,所以后者在中间or前面插入元素,或者删除元素都会快很多。
defaultdict为新的键值添加了一个默认的工厂,可以避免编写一个额外的测试来初始化映射条目,比dict.setdefault更高效,引用python文档的一个例子:
#使用profilestats工具进行性能分析 >>>frompbp.scripts.profilerimportprofile,stats >>>s=[('yellow',1),('blue',2),('yellow',3), ...('blue',4),('red',1)] >>>@profile('defaultdict') ...deffaster(): ...d=defaultdict(list) ...fork,vins: ...d[k].append(v) ... >>>@profile('dict') ...defslower(): ...d={} ...fork,vins: ...d.setdefault(k,[]).append(v) ... >>>slower();faster() Optimization:Solutions [306] >>>stats['dict'] {'stones':16.587882671716077,'memory':396, 'time':0.35166311264038086} >>>stats['defaultdict'] {'stones':6.5733464259021686,'memory':552, 'time':0.13935494422912598}
可见性能提升了快3倍。defaultdict用一个list工厂作为参数,同样可用于内建类型,比如long等。
除了实现的算法、架构之外,python提倡简单、优雅。所以正确的语法实践又很有必要,这样才会写出优雅易于阅读的代码。
二、语法最佳实践
字符串操作:优于python字符串对象是不可改变的,因此对任何字符串的操作如拼接,修改等都将产生一个新的字符串对象,而不是基于原字符串,因此这种持续的copy会在一定程度上影响Python的性能:
(1)用join代替'+'操作符,后者有copy开销;
(2)同时当对字符串可以使用正则表达式或者内置函数来处理的时候,选择内置函数。如str.isalpha(),str.isdigit(),str.startswith((‘x',‘yz')),str.endswith((‘x',‘yz'))
(3)字符格式化操作优于直接串联读取:
str="%s%s%s%s"%(a,b,c,d) #efficient
str=""+a+b+c+d+"" #slow
2.善用listcomprehension(列表解析) &generator(生成器)&decorators(装饰器),熟悉itertools等模块:
(1)列表解析,我觉得是python2中最让我印象深刻的特性,举例1:
>>>#thefollowingisnotsoPythonic >>>numbers=range(10) >>>i=0 >>>evens=[] >>>whilei<len(numbers): >>>ifi%2==0:evens.append(i) >>>i+=1 >>>[0,2,4,6,8] >>>#thegoodwaytoiteratearange,elegantandefficient >>>evens=[iforiinrange(10)ifi%2==0] >>>[0,2,4,6,8]
举例2:
def_treament(pos,element): return'%d:%s'%(pos,element) f=open('test.txt','r') if__name__=='__main__': #listcomps1 printsum(len(word)forlineinfforwordinline.split()) #listcomps2 print[(x+1,y+1)forxinrange(3)foryinrange(4)] #func printfilter(lambdax:x%2==0,range(10)) #listcomps3 print[iforiinrange(10)ifi%2==0] #listcomps4pythonic print[_treament(i,el)fori,elinenumerate(range(10))] output: 24 [(1,1),(1,2),(1,3),(1,4),(2,1),(2,2),(2,3),(2,4),(3,1),(3,2),(3,3),(3,4)] [0,2,4,6,8] [0,2,4,6,8] ['0:0','1:1','2:2','3:3','4:4','5:5','6:6','7:7','8:8','9:9']
没错,就是这么优雅简单。
(2)生成器表达式在python2.2引入,它使用'lazyevaluation'思想,因此在使用内存上更有效。引用python核心编程中计算文件中最长的行的例子:
f=open('/etc/motd,'r') longest=max(len(x.strip())forxinf) f.close() returnlongest
这种实现简洁而且不需要把文件文件所有行读入内存。
(3)python在2.4引入装饰器,又是一个让人兴奋的特性,简单来说它使得函数和方法封装(接收一个函数并返回增强版本的函数)更容易阅读、理解。'@'符号是装饰器语法,你可以装饰一个函数,记住调用结果供后续使用,这种技术被称为memoization的,下面是用装饰器完成一个cache功能:
importtime importhashlib importpickle fromitertoolsimportchain cache={} defis_obsolete(entry,duration): returntime.time()-entry['time']>duration defcompute_key(function,args,kw): #序列化/反序列化一个对象,这里是用pickle模块对函数和参数对象进行序列化为一个hash值 key=pickle.dumps((function.func_name,args,kw)) #hashlib是一个提供MD5和sh1的一个库,该结果保存在一个全局字典中 returnhashlib.sha1(key).hexdigest() defmemoize(duration=10): def_memoize(function): def__memoize(*args,**kw): key=compute_key(function,args,kw) #dowehaveitalready if(keyincacheand notis_obsolete(cache[key],duration)): print'wegotawinner' returncache[key]['value'] #computing result=function(*args,**kw) #storingtheresult cache[key]={'value':result,- 'time':time.time()} returnresult return__memoize return_memoize @memoize() defvery_very_complex_stuff(a,b,c): returna+b+c printvery_very_complex_stuff(2,2,2) printvery_very_complex_stuff(2,2,2) @memoize(1) defvery_very_complex_stuff(a,b): returna+b printvery_very_complex_stuff(2,2) time.sleep(2) printvery_very_complex_stuff(2,2)
运行结果:
6 wegotawinner 6 4 4
装饰器在很多场景用到,比如参数检查、锁同步、单元测试框架等,有兴趣的人可以自己进一步学习。
3. 善用python强大的自省能力(属性和描述符):自从使用了python,真的是惊讶原来自省可以做的这么强大简单,关于这个话题,限于内容比较多,这里就不赘述,后续有时间单独做一个总结,学习python必须对其自省好好理解。
三、编码小技巧
1、在python3之前版本使用xrange代替range,因为range()直接返回完整的元素列表而xrange()在序列中每次调用只产生一个整数元素,开销小。(在python3中xrange不再存在,里面range提供一个可以遍历任意长度的范围的iterator)
2、ifdoneisnotNone比语句ifdone!=None更快;
3、尽量使用"in"操作符,简洁而快速:foriinseq:printi
4、'x<y<z'代替'x<yandy<z';
5、while1要比whileTrue更快,因为前者是单步运算,后者还需要计算;
6、尽量使用build-in的函数,因为这些函数往往很高效,比如add(a,b)要优于a+b;
7、在耗时较多的循环中,可以把函数的调用改为内联的方式,内循环应该保持简洁。
8、使用多重赋值来swap元素:
x,y=y,x #elegantandefficient
而不是:
temp=x
x=y
y=temp
9.三元操作符(python2.5后):V1ifXelseV2,避免使用(XandV1)orV2,因为后者当V1=""时,就会有问题。
10.python之switchcase实现:因为switchcase语法完全可用ifelse代替,所以python就没 有switchcase语法,但是我们可以用dictionary或lamda实现:
switchcase结构:
switch(var) { casev1:func1(); casev2:func2(); ... casevN:funcN(); default:default_func(); } dictionary实现: values={ v1:func1, v2:func2, ... vN:funcN, } values.get(var,default_func)() lambda实现: { '1':lambda:func1, '2':lambda:func2, '3':lambda:func3 }[value]()
用try…catch来实现带Default的情况,个人推荐使用dict的实现方法。
这里只总结了一部分python的实践方法,希望这些建议可以帮助到每一位使用python的同学,优化性能不是重点,高效解决问题,让自己写的代码更加易于维护!