提升Python程序运行效率的方法总结

使用Python中的timeit函数测试程序运行效率:

def timeit(stmt="pass", setup="pass", timer=,
           number=1000000, globals=None):
    """Convenience function to create Timer object and call timeit method."""
    return Timer(stmt, setup, timer, globals).timeit(number)
def repeat(stmt="pass", setup="pass", timer=,
           repeat=5, number=1000000, globals=None):
    """Convenience function to create Timer object and call repeat method."""
    return Timer(stmt, setup, timer, globals).repeat(repeat, number)

从上面代码中可见,无论是timeit还是repeat都是先生成Timer对象,然后调用了Timer对象的timeit或repeat函数。
在使用timeit模块时,可以直接使用timeit.timeit()和tiemit.repeat(),还可以先用timeit.Timer()来生成一个Timer对象,然后再用TImer对象的timeit()和repeat()函数,后者更灵活一些。
函数的参数:
1、stmt:用于传入要测试时间的代码,可以直接接受字符串的表达式,也可以接受单个变量,还可以接受函数。传入函数时要把函数申明在当前文件中,然后在 stmt = ‘func()’ 执行函数,最后使用 setup = ‘from main import func’
2、setup:传入stmt的运行环境,比如stmt中使用到的参数、变量,要导入的模块等。可以写一行语句,也可以写多行语句,写多行语句时要用分号;隔开语句。
3、number:要测试的代码的运行次数,默认1000000次,对于耗时的代码,运行太多次会比较慢,此时建议自己修改一下运行次数
4、repeat:指测试要重复几次,每次的结果构成列表返回,默认5次。
一、直接使用timeit.timeit()、tiemit.repeat():

import timeit 
print(timeit.timeit(stmt= 'list(i**2 for i in normal_list)',setup = 'normal_list=range(10000)',number=10))
#0.3437936799875755
print(timeit.repeat(stmt= 'list(i**2 for i in normal_list)', setup='normal_list=range(10000)',repeat=2,number=10))
#[0.33649995761778984, 0.3394490767789293]
#setup 为复合语句
print(timeit.timeit(stmt= 'list(i**2 for i in normal_list)',setup = 'a=10000;normal_list=range(a)',number=10))
#0.33272367424748817
print(timeit.repeat(stmt= 'list(i**2 for i in normal_list)', setup='a=10000;normal_list=range(a)',repeat=2,number=10))
#[0.3323106610316342, 0.3356380911962764]
def func():
    normal_list=range(10000)
    L = [i**2 for i in normal_list]
#stmt为函数
print(timeit.timeit("func()", setup="from __main__ import func",number=10))
#0.12436874684622312
print(timeit.repeat("func()", setup="from __main__ import func",repeat=2,number=10))
#[0.12142133435126468, 0.12079555675148601]

直接用函数的方式速度更快。
二、先生成Timer,再调用timeit()、repeat():

import timeit 
#生成timer
timer1 = timeit.Timer(stmt= 'list(i**2 for i in normal_list)',setup = 'normal_list=range(10000)')
#调用timeit和repeat时还传number和repeat参数
print(timer1.timeit(number=10))
#0.34721554568091145
print(timer1.repeat(repeat=2,number=10))
#[0.3391925079630199, 0.34103400077255097]
#setup 为复合语句
timer1 = timeit.Timer(stmt= 'list(i**2 for i in normal_list)',setup = 'a=10000;normal_list=range(a)')
print(timer1.timeit(number=10))
0.34383463997592467
print(timer1.repeat(repeat=2,number=10))
#[0.34573984832288773, 0.34413273766891006]
#stmt为函数
def func():
    normal_list=range(10000)
    L = [i**2 for i in normal_list]
timer1 = timeit.Timer("func()", setup="from __main__ import func")
print(timer1.timeit(number=10))
#0.1223264363160359
print(timer1.repeat(repeat=2,number=10))
#[0.12266321844246209, 0.1264150395975001]

优化方法:
1、 使用函数,局部变量比全局变量快很多。尽量使用函数,如main()
2、 有选择性的消除属性访问。如多用from math import sqrt 而不要直接在程序中多次调用 math.sqrt(),或直接声明局部变量。

import math
def compute_roots(nums):
    sqrt = math.sqrt
    res = []
    res_append = res.append
    for n in nums:
        res_append(sqrt(n))
    return res

# 微优化
a = { i:i**2 for i in range(1000) } # 较快
b = [dict(i=i**2) for i in range(1000) ] #较慢

3、 避免不必要的抽象,如装饰器,@property等
4、 使用内建的字符串,元组,列表,集合,字典等容器
5、 避免不必要的数据结构或拷贝动作 
6、 使用cPython或pypy等。
7、 优化之前,先要跑起来,先要有正确的算法!低复杂度的算法远比程序的优化更重要。
8、 使用内置操作符。
Python是一种解释型语言,基于高级抽象。所以应该尽可能使用内置的。这将使代码更有效率,因为内置的预编译和快速。而包括解释步骤在内的漫长迭代很慢。
同样,喜欢使用内置功能,如地图,显着改善速度。
9、在循环中限制方法查找。
在循环中工作时,应该缓存方法调用,而不是在对象上调用它。否则,方法查找是昂贵的。


提升Python程序运行效率的方法总结_第1张图片

10、使用字符串进行优化。
字符串连接缓慢,不要在循环中执行。而是使用Python的join方法。或者,使用格式化功能来形成一个统一的字符串。
Python中的RegEx操作很快就被推回到C代码。然而,在某些情况下,基本的字符串方法(如更好地工作。
此外,您可以使用模块测试不同的方法。它将帮助您确定哪种方法是真正最快的。
11、用If语句进行优化。
Python像大多数编程语言一样懒惰。如果加入“AND”条件,那么并非所有条件都将被测试,以防其中一个变为假。
a、可以调整代码使用Python的行为。例如,要在列表中搜索固定模式,则可以通过添加以下条件来减小范围。
如果目标字符串的大小小于模式的长度,则添加一个“AND”条件,该条件变为false。
此外,可以先测试一个快速条件(如果有的话),如“string应该以@开头”或“string应该以点结尾”。
b、可以测试比< > 更快的条件< not >。例如: if done is None if done != None
12、 排序时使用键(key)
有很多老的Python排序代码,在创建一个自定义的排序时花费时间,但在运行时却能加速执行排序过程。元素排序的最好方法是尽可能使用键(key)和默认的sort()排序方法。例如:

import operator
somelist = [(1, 5, 8), (6, 2, 4), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(0))
somelist
#Output = [(1, 5, 8), (6, 2, 4), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(1))
somelist
#Output = [(6, 2, 4), (1, 5, 8), (9, 7, 5)]
somelist.sort(key=operator.itemgetter(2))
somelist
#Output = [(6, 2, 4), (9, 7, 5), (1, 5, 8)]

每一个实例中,根据选择作为key参数部分的索引,数组进行了排序。类似于利用数字进行排序,这种方法同样适用于利用字符串排序。
13、 优化循环
每种编程语言都会强调需要优化循环。当使用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原生的解释器在那种情况下,真的会减缓执行的速度。
(注意:优化循环的方法有很多,这只是其中的一个。例如,许多程序员都会说,列表推导即列表解析,是在循环中提高执行速度的最好方式(其实生成器表达式更快,能用就尽量用)。这里的关键是,优化循环是程序取得更高的执行速度的更好方式之一。)
14、 使用较新版本的Python
在网上搜索Python信息,都会发现无数人在问,从Python一个版本迁移到另一个版本的问题的信息。一般来说,Python的每一个版本都包含了能让其比上个版本运行更快的优化。版本迁移的限制因素是,你喜欢的那些库是否已经迁移到Python的较新版本。相比于询问是否应该进行版本迁移,关键问题是确定一个新版本什么时候有足够的支持,以保证迁移的可行性。
需要验证代码仍然运行。需要在Python的新版本下使用已获得的新库,然后检查应用程序是否需要重大改变。只有在作出必要的更正之后,才会注意到版本之间的差别。然而,如果正好应用程序能在新版本下运行,而不需要任何改变,则可能会错过那些版本升级带来的新特性。一旦进行了迁移,应该为新版本下的应用程序写一个说明,检查有问题的地方,并且优先考虑利用新版本的特性去更新那些地方。这样用户将会在升级的过程中更早的看到一个更大的性能提升。
15、 尝试多种编码方法
如果每次创建一个应用程序都是用相同的编码方法,几乎肯定会导致一些应用程序比它能够达到的运行效率慢的情况。作为分析过程的一部分,可以尝试一些实验。例如,在一个字典中管理一些元素,可以采用安全的方法确定元素是否已经存在并更新,或者可以直接添加元素,然后作为异常处理该元素不存在情况。考虑第一个编码的例子:

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)

两种情况下具有相同的输出:{‘d’: 4, ‘c’: 4, ‘b’: 4, ‘a’: 4}。唯一的不同是这个输出是如何得到的。跳出固定的思维模式,创造新的编码技巧,能够使应用程序获得更快的结果。
16、 交叉编译应用程序
开发者有时会忘记,电脑实际上是不懂任何用于创建现代应用程序的语言,电脑所能懂得是机器代码。为了能在电脑上运行应用程序,使用一个应用将人类可读的代码转换成计算机能理解的。有时候用一种语言,比如Python,写一个应用,并用另一种语言,比如C++,运行它,从性能的角度来看是有意义的。这取决于想要应用程序去做什么,以及主机系统可以提供的资源。
一个有趣的交叉编译器,Nuitka,可以将Python代码转换为C++代码。这么做的结果是,可以在原生模式下执行应用程序,而不是依靠解释器。根据平台和任务,可以看到一个显著的性能提升。
(注意:Nuitka目前还处于测试阶段,所以用它来产品程序时需要小心。实际上,目前最好将其用于实验。现在也有一些关于交叉编译是否是得到更好性能的最佳方式的讨论。开发者已经利用交叉编译好几年了,目的是实现特定的目标,比如更好的应用程序的速度。记住,每一个解决方案都会有得有失,应该在将一个解决方案用于生产环境之前就好好考虑一下得失情况。)
在使用一个交叉编译器时,要确保它支持你使用的Python的版本。Nuitka支持Python2.6、2.7、3.2和3.3。想让这个方案发挥作用,需要一个Python解释器和一个C++编译器。Nuitka支持多种C++编译器,包括Microsoft Visual Studio、MinGW 和 Clang/LLVM。
交叉编译也可能带来一些严重的负面影响。例如,当利用Nuitka工作时,会发现即使一个小程序也能消耗很大的硬盘空间,这是因为Nuitka使用大量的动态链接库(DLLs)实现Python的功能。所以面对一个资源有限的系统时,这个方案可能不会很好的起作用。

总结:
1、使用函数;
2、尽量不使用全局变量;
3、多用from math import sqrt而不要直接在程序中多次调用 math.sqrt();
4、尽量使用内建的字符串,元组,列表,集合,字典等容器;
5、变量缓存,避免多次计算,使之可重复调用;
6、使用if语句优化;
7、元素排序尽可能使用键(key)和默认的sort()排序方法;
8、优化循环:避免在循环中使用点操作(类似第5点,如:循环中每次调用方法str.upper,Python都会求该方法的值。如果用一个变量代替求得的值,值就变成已知的,可以更快地执行。),尽可能使用生成器表达式(速度最快)。

补充:
1 、优化算法时间复杂度
算法的时间复杂度对程序的执行效率影响最大,在Python中可以通过选择合适的数据结构来优化时间复杂度,如list和set查找某一个元素的时间复杂度分别是O(n)和O(1)。不同的场景有不同的优化方式,总得来说,一般有分治,分支界限,贪心,动态规划等思想。

2 、减少冗余数据
如用上三角或下三角的方式去保存一个大的对称矩阵。在0元素占大多数的矩阵里使用稀疏矩阵表示。

3 、合理使用copy与deepcopy
对于dict和list等数据结构的对象,直接赋值使用的是引用的方式,而有些情况下需要复制整个对象,这时可以使用copy包里的copy和deepcopy,这两个函数的不同之处在于后者是递归复制的。效率也不一样:(以下程序在ipython中运行)

import copy
a = range(100000)
%timeit -n 10 copy.copy(a) 			#运行10次copy.copy(a)
%timeit -n 10 copy.deepcopy(a)
10 loops, best of 3: 1.55 ms per loop
10 loops, best of 3: 151 ms per loop

timeit后面的-n表示运行的次数,后两行对应的是两个timeit的输出,下同。由此可见后者慢一个数量级。

4 、使用dict或set查找元素
Python中dict和set都是使用hash表来实现(类似c++11标准库中unordered_map),查找元素的时间复杂度是O(1)

a = range(1000)
s = set(a)
d = dict((i,1) for i in a)
%timeit -n 10000 100 in d
%timeit -n 10000 100 in s
10000 loops, best of 3: 43.5 ns per loop
10000 loops, best of 3: 49.6 ns per loop

dict的效率略高(占用的空间也多一些)。

5 、合理使用生成器(generator)和yield

%timeit -n 100 a = (i for i in range(100000))
%timeit -n 100 b = [i for i in range(100000)]
100 loops, best of 3: 1.54 ms per loop
100 loops, best of 3: 4.56 ms per loop

使用()得到的是一个generator对象,所需要的内存空间与列表的大小无关,所以效率会高一些。在具体应用上,比如set(i for i inrange(100000))会比set([i for i in range(100000)])快。但是对于需要循环遍历的情况:

%timeit -n 10 for x in (i for i in range(100000)): pass
%timeit -n 10 for x in [i for i in range(100000)]: pass
10 loops, best of 3: 6.51 ms per loop
10 loops, best of 3: 5.54 ms per loop

后者的效率反而更高,但是如果循环里有break,用generator的好处是显而易见的。yield也是用于创建generator:

def yield_func(ls):
	for i in ls:
		yield i+1

def not_yield_func(ls):
	return [i+1 for i in ls]

ls = range(1000000)
%timeit -n 10 for i in yield_func(ls):pass
%timeit -n 10 for i in not_yield_func(ls):pass
10 loops, best of 3: 63.8 ms per loop
10 loops, best of 3: 62.9 ms per loop

对于内存不是非常大的list,可以直接返回一个list,但是可读性yield更佳,python2.x内置generator功能的有xrange函数、itertools包等。

6 、优化循环
循环之外能做的事不要放在循环内,比如下面的优化可以快一倍:

a = range(10000)
size_a = len(a)
%timeit -n 1000 for i in a: k = len(a)
%timeit -n 1000 for i in a: k = size_a
1000 loops, best of 3: 569 ?s per loop
1000 loops, best of 3: 256 ?s per loop

7 、优化包含多个判断表达式的顺序
对于and,应该把满足条件少的放在前面,对于or,把满足条件多的放在前面。如:

a = range(2000)
%timeit -n 100 [i for i in a if 10 < i < 20 or 1000 < i < 2000]
%timeit -n 100 [i for i in a if 1000 < i < 2000 or 100 < i < 20]
%timeit -n 100 [i for i in a if i % 2 == 0 and i > 1900]
%timeit -n 100 [i for i in a if i > 1900 and i % 2 == 0]
100 loops, best of 3: 287 ?s per loop
100 loops, best of 3: 214 ?s per loop
100 loops, best of 3: 128 ?s per loop
100 loops, best of 3: 56.1 ?s per loop

8 、使用join合并迭代器中的字符串

In [1]: %%timeit
...: s = ''
...: for i in a:
...:	s += i
...:
10000 loops, best of 3: 59.8 ?s per loop
In [2]: %%timeit
s = ''.join(a)
...:
100000 loops, best of 3: 11.8 ?s per loop

join对于累加的方式,有大约5倍的提升。

9 、选择合适的格式化字符方式

s1, s2 = 'ax', 'bx'
%timeit -n 100000 'abc%s%s' % (s1, s2)
%timeit -n 100000 'abc{0}{1}'.format(s1, s2)
%timeit -n 100000 'abc' + s1 + s2
100000 loops, best of 3: 183 ns per loop
100000 loops, best of 3: 169 ns per loop
100000 loops, best of 3: 103 ns per loop

三种情况中,%的方式是最慢的,但是三者的差距并不大(都非常快)。

10、不借助中间变量交换两个变量的值

In [3]: %%timeit -n 10000
a,b=1,2
....: c=a;a=b;b=c;
....:
10000 loops, best of 3: 172 ns per loop
In [4]: %%timeit -n 10000
a,b=1,2
a,b=b,a
....:
10000 loops, best of 3: 86 ns per loop

使用a,b=b,a而不是c=a;a=b;b=c;来交换a,b的值,可以快1倍以上。

11、使用if is

a = range(10000)
%timeit -n 100 [i for i in a if i == True]
%timeit -n 100 [i for i in a if i is True]
100 loops, best of 3: 531 ?s per loop
100 loops, best of 3: 362 ?s per loop

使用 if is True 比 if == True 将近快一倍。

12、使用级联比较x < y < z

x, y, z = 1,2,3
%timeit -n 1000000 if x < y < z:pass
%timeit -n 1000000 if x < y and y < z:pass
1000000 loops, best of 3: 101 ns per loop
1000000 loops, best of 3: 121 ns per loop

x < y < z效率略高,而且可读性更好。

13、Python2中while 1比while True更快,Python3中一样

def while_1():
	n = 100000
	while 1:
		n -= 1
		if n <= 0: break
def while_true():
	n = 100000
	while True:
		n -= 1
		if n <= 0: break
m, n = 1000000, 1000000
%timeit -n 100 while_1()
%timeit -n 100 while_true()
100 loops, best of 3: 3.69 ms per loop
100 loops, best of 3: 5.61 ms per loop

while 1比 while true快很多,原因是在python2.x中,True是一个全局变量,而非关键字。

14、使用**而不是pow

%timeit -n 10000 c = pow(2,20)
%timeit -n 10000 c = 2**20
10000 loops, best of 3: 284 ns per loop
10000 loops, best of 3: 16.9 ns per loop

**就是快10倍以上!

15、使用cProfile, cStringIO和cPickle等用c实现相同功能(分别对应profile, StringIO, pickle)的包

import cPickle
import pickle
a = range(10000)
%timeit -n 100 x = cPickle.dumps(a)
%timeit -n 100 x = pickle.dumps(a)
100 loops, best of 3: 1.58 ms per loop
100 loops, best of 3: 17 ms per loop

由 c 实现的包,速度快 10 倍以上!

16 、使用最佳的反序列化方式
下面比较了eval, cPickle, json方式三种对相应字符串反序列化的效率:

import json
import cPickle
a = range(10000)
s1 = str(a)
s2 = cPickle.dumps(a)
s3 = json.dumps(a)
%timeit -n 100 x = eval(s1)
%timeit -n 100 x = cPickle.loads(s2)
%timeit -n 100 x = json.loads(s3)
100 loops, best of 3: 16.8 ms per loop
100 loops, best of 3: 2.02 ms per loop
100 loops, best of 3: 798 ?s per loop

可见json比cPickle快近3倍,比eval快20多倍。

17、使用C扩展(Extension)
目前主要有CPython(python最常见的实现方式)原生API,ctypes,Cython,cffi三种方式,它们的作用是使得Python程序可以调用由C编译成的动态链接库,其特点分别是:
CPython原生API:通过引入Python.h头文件,对应的C程序中可以直接使用Python的数据结构。实现过程相对繁琐,但是有比较大的适用范围。
Ctypes:通常用于封装(wrap)C程序,让纯Python程序调用动态链接库(Windows中的dll或Unix中的so文件)中的函数。如果想要在python中使用已经有C类库,使用ctypes是很好的选择,有一些基准测试下,python2+ctypes是性能最好的方式。
Cython:Cython是CPython的超集,用于简化编写C扩展的过程,Cython的优点是语法简洁,可以很好地兼容numpy等包含大量C扩展的库。Cython的使得场景一般是针对项目中某个算法或过程的优化。在某些测试中,可以有几百倍的性能提升。
Cffi:cffi的就是ctypes在pypy(详见下文)中的实现,同进也兼容CPython。cffi提供了在python使用C类库的方式,可以直接在python代码中编写C代码,同时支持链接到已有的C类库。
使用这些优化方式一般是针对已有项目性能瓶颈模块的优化,可以在少量改动原有项目的情况下大幅度地提高整个程序的运行效率。

18、并行编程
因为GIL的存在,Python很难充分利用多核CPU的优势。但是可以通过内置的模块multiprocessing实现下面几种并行模式:
多进程:对于CPU密集型的程序,可以使用multiprocessing的Process,Pool等封装好的类,通过多进程的方式实现并行计算。但是因为进程中的通信成本比较大,对于进程之间需要大量数据交互的程序效率未必有大的提高。
多线程:对于IO密集型的程序,multiprocessing.dummy模块使 multiprocessing的接口封装threading,使得多线程编程也变得非常轻松 (比如可以使用Pool的map接口,简洁高效 )。
分布式:multiprocessing中的Managers类提供了可以在不同进程之共享数据的方式,可以在此基础上开发出分布式的程序。
不同的业务场景可以选择其中的一种或几种的组合实现程序性能的优化。

19、终级大杀器:PyPy
PyPy是用RPython(CPython的子集)实现的Python,根据官网的基准测试数据,它比CPython实现的Python要快6倍以上。快的原因是使用了Just-in-Time(JIT)编译器,即动态编译器,与静态编译器(如gcc,javac等)不同,它是利用程序运行的过程的数据进行优化。由于历史原因目前pypy中还保留着GIL不过正在进行的STM项目试图将PyPy变成没有GIL的Python。如果python程序中含有C扩展 (非cffi的方式),JIT的优化效果会大打折扣,甚至比CPython慢(比Numpy)所以在PyPy中最好用纯Python或使用cffi扩展。随着STM,Numpy等项目的完善,相信PyPy将会替代CPython。

20、使用性能分析工具
除了上面在ipython使用到的timeit模块,还有cProfilecProfile的使用方式也非常简单:python -m cProfile filename.py是要运行程序的文件名,可以在标准输出中看到每一个函数被调用的次数和运行的时间,从而找到程序的性能瓶颈,然后可以有针对性地优化。

你可能感兴趣的:(Python)