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的同学,优化性能不是重点,高效解决问题,让自己写的代码更加易于维护!