python速度技巧

需要多数据成员进行频繁的查找或者访问的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
from time import time 
t = time()
list = ['a','b','is','python','jason','hello','hill','with','phone','test',
'dfdf','apple','pddf','ind','basic','none','baecr','var','bana','dd','wrd']
#list = dict.fromkeys(list,True)
print list
filter = []
for i in range (1000000):
for find in ['is','hat','new','list','old','.']:
if find not in list:
filter.append(find)
print "total run time:"
print time()-t

上述代码运行大概需要 16.09seconds。如果去掉行 #list = dict.fromkeys(list,True) 的注释,将 list 转换为字典之后再运行,时间大约为 8.375 seconds,效率大概提高了一半。因此在需要多数据成员进行频繁的查找或者访问的时候,使用 dict 而不是 list 是一个较好的选择。

list 交集,并集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from time import time 
t = time()
lista=[1,2,3,4,5,6,7,8,9,13,34,53,42,44]
listb=[2,4,6,9,23]
intersection=[]
for i in range (1000000):
for a in lista:
for b in listb:
if a == b:
intersection.append(a)


print "total run time:"
print time()-t
#total run time:
#38.4070000648

而是用set求交集

1
2
3
4
5
6
7
8
9
10
11
from time import time 
t = time()
lista=[1,2,3,4,5,6,7,8,9,13,34,53,42,44]
listb=[2,4,6,9,23]
intersection=[]
for i in range (1000000):
list(set(lista)&set(listb))
print "total run time:"
print time()-t
#total run time:
# 8.75

对列表/循环的优化

地下这段代码大概运行时间是132.375

1
2
3
4
5
6
7
8
9
10
from time import time 
t = time()
lista = [1,2,3,4,5,6,7,8,9,10]
listb =[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.01]
for i in range (1000000):
for a in range(len(lista)):
for b in range(len(listb)):
x=lista[a]+listb[b]
print "total run time:"
print time()-t

现在进行如下优化,将长度计算提到循环外,range 用 xrange 代替,同时将第三层的计算 lista[a] 提到循环的第二层。

1
2
3
4
5
6
7
8
9
10
11
12
13
from time import time 
t = time()
lista = [1,2,3,4,5,6,7,8,9,10]
listb =[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,0.01]
len1=len(lista)
len2=len(listb)
for i in xrange (1000000):
for a in xrange(len1):
temp=lista[a]
for b in xrange(len2):
x=temp+listb[b]
print "total run time:"
print time()-t

上述优化后的程序其运行时间缩短为 102.171999931
xrange的用法与range相同,即xrange(start, stop, step)根据start与stop指定的范围以及step设定的步长,他所不同的是xrange并不是生成序列,而是作为一个生成器。即他的数据生成一个取出一个。
当列表特别大时,使用xrange省内存。

使用列表解析(list comprehension)和生成器表达式(generator expression)

列表解析要比在循环中重新构建一个新的 list 更为高效,因此我们可以利用这一特性来提高运行的效率。

1
2
3
4
5
6
7
8
9
10
from time import time 
t = time()
list = ['a','b','is','python','jason','hello','hill','with','phone','test',
'dfdf','apple','pddf','ind','basic','none','baecr','var','bana','dd','wrd']
total=[]
for i in range (1000000):
for w in list:
total.append(w)
print "total run time:"
print time()-t

列表解析

1
2
for i in range (1000000): 
a = [w for w in list]

上述代码直接运行大概需要 17s,而改为使用列表解析后 ,运行时间缩短为 9.29s。将近提高了一半。生成器表达式则是在 2.4 中引入的新内容,语法和列表解析类似,但是在大数据量处理时,生成器表达式的优势较为明显,它并不创建一个列表,只是返回一个生成器,因此效率较高。在上述例子上中代码 a = [w for w in list] 修改为 a = (w for w in list),运行时间进一步减少,缩短约为 2.98s。

字符串的优化

python 中的字符串对象是不可改变的,因此对任何字符串的操作如拼接,修改等都将产生一个新的字符串对象,而不是基于原字符串,因此这种持续的 copy 会在一定程度上影响 python 的性能。
在字符串连接的使用尽量使用 join() 而不是 +:在底下这段代码中中使用 + 进行字符串连接大概需要 0.125 s,而使用 join 缩短为 0.016s。因此在字符的操作上 join 比 + 要快,因此要尽量使用 join 而不是 +。

1
2
3
4
5
6
7
8
9
10
from time import time 

t = time()
s = ""
list = ['a','b','b','d','e','f','g','h','i','j','k','l','m','n']
for i in range (10000):
for substr in list:
s+= substr
print "total run time:"
print time()-t

对字符进行格式化比直接串联读取要快,因此要使用
out = “%s%s%s%s“ % (head, prologue, query, tail)
而不是 out = ““ + head + prologue + query + tail + ““

Cpython

Cython 是用 python 实现的一种语言,可以用来写 python 扩展,用它写出来的库都可以通过 import 来载入,性能上比 python 的快。
Cython 代码与 python 不同,必须先编译,编译一般需要经过两个阶段,将 pyx 文件编译为 .c 文件,再将 .c 文件编译为 .so 文件。编译有多种方法,自行查阅
简单的性能比较·
底下是cpython

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from time import time 
def test(int n):
cdef int a =0
cdef int i
for i in xrange(n):
a+= i
return a

t = time()
test(10000000)
print "total run time:"
print time()-t
#total run time:
#0.00714015960693

Python 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
from time import time 
def test(n):
a =0;
for i in xrange(n):
a+= i
return a
t = time()
test(10000000)
print "total run time:"
print time()-t
#total run time:
#0.971596002579

从上述对比可以看到使用 Cython 的速度提高了将近 100 多倍。

定位程序性能瓶颈

对代码优化的前提是需要了解性能瓶颈在什么地方,程序运行的主要时间是消耗在哪里,对于比较复杂的代码可以借助一些工具来定位,python 内置了丰富的性能分析工具,如 profile,cProfile 与 hotshot 等。其中 Profiler 是 python 自带的一组程序,能够描述程序运行时候的性能,并提供各种统计帮助用户定位程序的性能瓶颈。Python 标准模块提供三种 profilers:cProfile,profile 以及 hotshot。

profile 的使用非常简单,只需要在使用之前进行 import 即可。具体实例如下:

1
2
3
4
5
6
7
8
9
import profile 
def profileTest():
Total =1;
for i in range(10):
Total=Total*(i+1)
print Total
return Total
if __name__ == "__main__":
profile.run("profileTest()")

输出结果解析:
ncalls:表示函数调用的次数;
tottime:表示指定函数的总的运行时间,除掉函数中调用子函数的运行时间;
percall:(第一个 percall)等于 tottime/ncalls;
cumtime:表示该函数及其所有子函数的调用运行的时间,即函数开始调用到返回的时间;
percall:(第二个 percall)即函数运行一次的平均时间,等于 cumtime/ncalls;
filename:lineno(function):每个函数调用的具体信息;

其他

1 if done is not None 比语句 if done != None 更快,读者可以自行验证;
2 使用级联比较 “x < y < z” 而不是 “x < y and y < z”;
3 while 1 要比 while True 更快(当然后者的可读性更好);
4 build in 函数通常较快,add(a,b) 要优于 a+b。