[译]Python Performance

性能技巧
目录
目录 1
其他版本 2
概述:优化需要优化的 2
选择正确的数据结构 2
排序 2
字符串连接 4
循环 4
避免“点”... 5
局部变量 5
初始化字典元素 6
import语句的消耗 7
数据聚合 8
少做 9
Python不是C 9
使用xrange来代替range 11
执行时绑定函数 12
性能分析代码 12
性能分析 13
cProfile和Hotshot模块 13
Trace模块 13
可视化性能评估结果 13


这篇文章提供了很多可以提高你python程序效率的技巧和窍门。如果这条技巧是从其他某个地方吸收过来的,我会标明来源。
从我1996年写“fast python”以来Python在很大程度上已经不一样了,意思是很多命令已作了相应的修改。我把它挪到Python wiki上去,希望能有其他人能够一起来维护它。
小贴士:你应该随时检查你的应用是否遵守这些技巧并明确你将使用的Python版本,而不是盲目的认为其中一个措施一定比另一个快。更多的请参考具体的性能分析部分。
像Cpython、Pyrex、Psyco、Weave、Shed Skin和PyInline这些新的工具包,可以轻易的让性能瓶颈代码提高到C或者机器语言上,从而显著的提高应用的性能。
其他版本
俄语:http://omsk.lug.ru/wacko/PythonHacking/PerfomanceTips
概述:优化需要优化的
首先在让程序执行得到正确结果的前提下,你只需要知道什么让你的程序执行缓慢,然后执行它验证正确的程序是否缓慢。
当发现运行缓慢时,性能分析能够显示出程序的那个部分消耗了大量的时间。一个全面的但是可以快速执行的测试套件就可以保证未来的优化不会改变你程序的正确性。

简而言之:

1. 让程序成功运行
2. 测试它是正确的
3. 分析是否执行缓慢
4. 优化
5. 从第二步重复执行

某些优化归结到良好的编程规范,这些是在你学习这门语言的时候需要去学习的。一个例子就是把循环中不会有值变化的计算移到循环外。
选择正确的数据结构
待定。
排序
排序Python基本对象的列表是非常高效的。列表的sort方法提供了一个可选的比较函数作为一个参数,从而可以改变排序的行为。这是非常方便的,但是可能会在一定程度上让你的排序变慢,因为这个比较函数会调用很多次。在Python 2.4中,你应该使用内置的sort关键字来代替,这会是最快的排序方法。
只有你使用过时的Python版本时(2.4之前)下面在Guido van Rossum上提到的建议才有效:
另一个可以加快排序的办法是构造一个元组列表,其中的元组的第一个元素是可以使用默认比较函数的排序键值,它的第二个元素是原始的列表元素。这就是所谓的施瓦茨变换,也就是熟知装饰排序去装饰(Decorate Sort Undecorate (DSU))。
例如,你有一个元组的列表,你想要按元组的第n个字段来排序。下边的代码将会完成这个功能:
def sortby(somelist, n):
    nlist = [(x[n], x) for x in somelist]
    nlist.sort()
    return [val for (key, val) in nlist]
当前这个列表排序的方法(原地排序)得到的效果,也可以简单的用下边的代码来完成:
def sortby_inplace(somelist, n):
    somelist[:] = [(x[n], x) for x in somelist]
    somelist.sort()
    somelist[:] = [val for (key, val) in somelist]
    return
下边是一个使用的例子:
>>> somelist = [(1, 2, 'def'), (2, -4, 'ghi'), (3, 6, 'abc')]
>>> somelist.sort()
>>> somelist
[(1, 2, 'def'), (2, -4, 'ghi'), (3, 6, 'abc')]
>>> nlist = sortby(somelist, 2)
>>> sortby_inplace(somelist, 2)
>>> nlist == somelist
True
>>> nlist = sortby(somelist, 1)
>>> sortby_inplace(somelist, 1)
>>> nlist == somelist
True
来自Tim Delaney
在Python 2.3中的sort保证了排序的稳定性。
(更准确的说,在CPython 2.3中排序似乎稳定的, 在Python2.4中也保证了稳定性。)

Python 2.4添加了一个可选的关键字,让变换变的简单了许多:
# E.g. n = 1
n = 1
import operator
nlist.sort(key=operator.itemgetter(n))
# use sorted() if you don't want to sort in-place:
# sortedlist = sorted(nlist, key=operator.itemgetter(n))
注意初始的元素一直没有用来排序,只用返回的键值,这和下边的代码是等价的:
# E.g. n = 1
n = 1
nlist = [(x[n], i, x) for (i, x) in enumerate(nlist)]
nlist.sort()
nlist = [val for (key, index, val) in nlist]
字符串连接
注解:这一部分的准确度,在后续版本的Python中有争论的地方。在CPython 2.5中,字符串连接是相当快速的,这在其他Python版本的实现中可能并不同样的适用。可以查看 ConcatenationTestCode的代码来讨论一下。

字符串在Python中是不可变的。这个特性让很多Python的新手觉得很难以理解,并吃了亏。不可变的特性同时给予了利与弊。好的地方,字符串可以作为字典的键,单个的拷贝可以被多个变量绑定共享。(Python自动共享有一个-和两个-的字符串。)不好的地方是, 你不能说:把任意字符串中的所有a替换为b。因此你不得不重新创建一个有这个特性的字符串。这种连续的拷贝在Python程序中可能导致很严重的效率低下。
避免下边的情况:
s = ""
for substring in list:
    s += substring
使用 s = "".join(list) 代替。上一个方法在创建很大的字符串时是一个很常见和严重的错误。同样的,如果你要逐位生产一个字符串,使用下面的代码来代替:
s = ""
for x in list:
    s += some_function(x)
使用
slist = [some_function(elt) for elt in somelist]
s = "".join(slist)
避免:
out = "<html>" + head + prologue + query + tail + "</html>"
用下边的来代替:
out = "<html>%s%s%s%s</html>" % (head, prologue, query, tail)
更好的是,为了可读性(这对作为一个程序员的你比你程序的效率更重要),使用字典代替:
out = "<html>%(head)s%(prologue)s%(query)s%(tail)s</html>" % locals()
前两个执行起来要更快,尤其是在放在许多的CGI脚本里面,而且更容易修改。此外, 用较慢的方式来做事情在Python 2.0还会因为更多的比较而更慢。现在让Python的虚拟机计算出怎么连接两个字符串要花费更长的时间。(不要忘了Python在执行的时候会遍历所有的方法。)
循环
Python支持多重循环的结构。for语句是使用频率最高的。它循环遍历一个序列的元素,把每个赋值给循环变量。如果你的循环体很简单,在解释器的所有的开销中for循环本身的开销要占很大的一部分。这里就可以体现map函数的方便之处了。你可以把map想象成C代码中的for循环。唯一的约束是map的“循环体”必须是函数调用。除了列表解析在语法上的优势,他们和map一样快或者说更快。
这里有一个直观的例子。循环一个列表,并把它们都转换成大写字母:
newlist = []
for word in oldlist:
    newlist.append(word.upper())
你可以使用map函数让编译后的C代码来代替解释器来执行循环:
newlist = map(str.upper, oldlist)
列表解析,是在Python 2.0 的时候加入的。它们能提供一种更紧凑、更高效的方式来写循环:
newlist = [s.upper() for s in oldlist]
生成器表达式在Python 2.4被加入。他们执行的或多或少和列表解析和map有些类似,但是避免了一下子生成整个列表。作为替代,它会返回一个能够迭代的生成器对象:
iterator = (s.upper() for s in oldlist)
这种方式会根据你使用的Python版本和你正在操作数据的特性来适配。
Guido van Rossum 写了一个毫无疑问更加值得一读的文章,它更加细节(简洁)的循环优化。
避免“点”...
设想你不能使用map或者列表解析?你会在for循环中难以自拔。这个for循环例子有另外的一个效率低下的地方。newlist.append和word.upper都是方法引用,在每次循环的时候都要重新计算。最开始的循环可以用下边的代码来替代:
upper = str.upper
newlist = []
append = newlist.append
for word in oldlist:
    append(upper(word))
这种技巧在使用的时候应该注意。当循环变的很大的时候会很难维护。除非你对那个片段的代码非常熟悉,你需要去查找append和upper在代码中的定义。
局部变量
我们最后可以加快程序的地方是这个非map版的for循环,局部变量可以在任意位置使用。如果上边的循环放在一个函数中,append和upper变成了一个局部变量。Python访问局部变量要比全局变量更快捷和高效。
def func():
    upper = str.upper
    newlist = []
    append = newlist.append
    for word in oldlist:
        append(upper(word))
    return newlist
在写这个代码的时候,我使用的环境是一个100MHz奔腾,跑得是BSD指令集。把/usr/share/dict/words(38470个单词)单词列表转换成大写的时间:
Version Time (seconds)
Basic loop 3.47
Eliminate dots 2.45
Local variable & no dots 1.79
Using map function 0.54
初始化字典元素
假设你在创建一个单词频率的字典,现在你已经把你的文本分解成了一个单词列表。你写的代码可能会和下边相似:
wdict = {}
for word in words:
    if word not in wdict:
        wdict[word] = 0
    wdict[word] += 1
除了第一次,每次处理一个单词都会执行if条件判断语句,但是都会失败。如果你计算很大量的单词,很多都会重复出现很多次。另一种情况,一个值的初始化可能只会发生一次,但是这个值会出现很多次,那么使用try语句花销会更小:
wdict = {}
for word in words:
    try:
        wdict[word] += 1
    except KeyError:
        wdict[word] = 1
捕捉KeyError异常很重要,不要有一个默认的except语句来避免在try语句中你不能处理的情况。
第三个选择在Pythonx2.x版本上可以使用。字典现在有一个get()方法,如果想要的键在字典中不存在的话,它能返回一个默认值。这个和下边额循环类似:
wdict = {}
get = wdict.get
for word in words:
    wdict[word] = get(word, 0) + 1
我最初写这个部分的时候,有很清楚的条件前面两种方式中的一个会更快。但是现在这三个方法表现出了相似的性能(相差在10%10%以内),他们或多或少和列表中的单词属性没有关系。
同时,如果存储在字典中的值是对象或者一个(可变)列表,你也可以使用dict.setdefault方法:
wdict.setdefault(key, []).append(new_element)
你可能会认为,这样的措施会查找两次键值。但实际上即使在Python 3.0中也没有,在C语言中却至少会执行两次查询。
另一个选择是似乎用defaultdict类:
from collections import defaultdict

wdict = defaultdict(int)

for word in words:
    wdict[word] += 1
import语句的消耗
import语句可以在任何位置被执行。把它们放在函数内来限制包的可见范围是很有用的,或者减少了初始启动的时间。虽然Python的解释器已经被优化,不会第二次导入相同名字的模块,重复执行导入语句在一些情形下是非常影响效率的。
考虑下边两个代码片段(收录自Greg McFarlane,我确定我是从comp.lang.python python-list@python.org的邮件列表中发现的未署名的,后来才在其他的资源里发现署了他的名字):
def doit1():
    import string ###### import statement inside function
    string.lower('Python')

for num in range(100000):
    doit1()
或者:
import string ###### import statement outside function
def doit2():
    string.lower('Python')

for num in range(100000):
    doit2()
doit2会比doit1执行的更快,即使这个模块的引用是全局的。这里有一个在Python 2.3下执行的会话,新的timeit模块显示了第二个模块到底比第一个快多少:
>>> def doit1():
... import string
... string.lower('Python')
...
>>> import string
>>> def doit2():
... string.lower('Python')
...
>>> import timeit
>>> t = timeit.Timer(setup='from __main__ import doit1', stmt='doit1()')
>>> t.timeit()
11.479144930839539
>>> t = timeit.Timer(setup='from __main__ import doit2', stmt='doit2()')
>>> t.timeit()
4.6661689281463623
字符串方法在Python 2.0时被引入。这样就提供了一个版本可以完全避免import,而且还可以执行的很快:
def doit3():
    'Python'.lower()

for num in range(100000):
    doit3()
下边是timeit的结论:
>>> def doit3():
... 'Python'.lower()
...
>>> t = timeit.Timer(setup='from __main__ import doit3', stmt='doit3()')
>>> t.timeit()
2.5606080293655396
上边的例子人为的因素太多,但是它们仍然把握了原则。
值得注意的是,把import语句放在函数中可以加快模块加载的初始化时间,尤其当要被导入的函数在初始化的时候不是必须的时候。这个就是懒惰优化的一个例子 – 避免操作,知道你确定要用这个模块的时候(导入一个模块的花销是很大)。
这将是一个非常可观的节约,当模块不会被导入的时候(从任何模块中) -- 如果这个模块已经被加载(像其他很多的标准模块,例如string和re),避免重复导入不会节约任何东西。你可以使用sys.modules来查看哪些模块已经被加载到了运行环境中。
一个既简单又好的导入办法:
email = None

def parse_email():
    global email
    if email is None:
        import email
    ...
这样email模块就只会被导入一次,仅仅在第一次调用parse_email()的时候。
数据聚合
函数调用在Python中也性能的消耗也是很高的,尤其是和内建函数比起来。强烈的建议在合适的情况下,函数应该处聚合的数据。这里是一个人工的Python例子:
import time
x = 0
def doit1(i):
    global x
    x = x + i

list = range(100000)
t = time.time()
for i in list:
    doit1(i)
print "%.3f" % (time.time()-t)
和:
import time
x = 0
def doit2(list):
    global x
    for i in list:
        x = x + i

list = range(100000)
t = time.time()
doit2(list)
print "%.3f" % (time.time()-t)
这里是用解释器会话来得出证据:
>>> t = time.time()
>>> for i in list:
... doit1(i)
...
>>> print "%.3f" % (time.time()-t)
0.758
>>> t = time.time()
>>> doit2(list)
>>> print "%.3f" % (time.time()-t)
0.204
即使用Python来写,第二个例子也要比第一个快4倍以上。如果用C来写doit函数,差别还将更大(把Python的for循环换成C的循环,和去掉所有的函数调用是一样的效果)。
少做
Python的解释器会执行以下周期性的检查。尤其是,它在决定是不是要让另一个线程运行的时候(尤其是和一个信号处理器相关联的时候)。很多情况下,这个是没有办法的,所以在每次执行检查时,每次这个解释器循环会让执行变慢。在sys模块中有一个函数,setcheckinterval,你可以用来告诉解释器执行检查的频率。在Python 2.3发布之前,默认值是10。在2.3里这个值被提高到了100。如果你没有执行任何的线程,你不希望它捕获很多的信号,那么把这个值设置很大将会提高解释器的性能,实际上大多数情况这样做是可行的。
Python不是C
它也不是Perl,Java,C++或者Haskell。在从其他语言迁移到python的时候要注意他们的区别。一个简单的例子示范:
% timeit.py -s 'x = 47' 'x * 2'
1000000 loops, best of 3: 0.574 usec per loop
% timeit.py -s 'x = 47' 'x << 1'
1000000 loops, best of 3: 0.524 usec per loop
% timeit.py -s 'x = 47' 'x + x'
1000000 loops, best of 3: 0.382 usec per loop
现在考虑一下类似的C程序(只显示了add函数):
#include <stdio.h>

int main (int argc, char *argv[]) {
 int i = 47;
 int loop;
 for (loop=0; loop<500000000; loop++)
  i + i;
 return 0;
}
执行时间:
% for prog in mult add shift ; do
< for i in 1 2 3 ; do
< echo -n "$prog: "
< /usr/bin/time ./$prog
< done
< echo
< done
mult: 6.12 real 5.64 user 0.01 sys
mult: 6.08 real 5.50 user 0.04 sys
mult: 6.10 real 5.45 user 0.03 sys

add: 6.07 real 5.54 user 0.00 sys
add: 6.08 real 5.60 user 0.00 sys
add: 6.07 real 5.58 user 0.01 sys

shift: 6.09 real 5.55 user 0.01 sys
shift: 6.10 real 5.62 user 0.01 sys
shift: 6.06 real 5.50 user 0.01 sys
注意,Python在执行一个数字加上本身的时候有很明显的优势,而不是乘以2或者左移一位。在C中,在所有的现代计算机体系结构中,上边的三个算术操作都被解释成了一个单独循环执行的计算机指令,所以你选择哪种运算是没有关联的。
一个常用来测试Python新手的代码是把下边的Perl片段:
while (<>) {
    print;
}
转换成Python代码会看起来像下边的样子:
import fileinput

for line in fileinput.input():
    print line,
用来说明Python肯定会比Perl慢。由于其他很多人已经说了很多次,Python在某些情况下比Perl慢,但是某些情况下又要比Perl快。相对的性能也会因为你对这两门语言的经验而不同。
使用xrange来代替range
小贴士:如果你使用的是Python 3.0,这个部分将不再适用。因为在Python 3.0里range提供了一个迭代器,可以遍历任意的大小,同时xrange已经不再存在。

Python有两种方式来获得一个范围的数:range和xrange。所有的人都知道range,因为它的名字就是它功能。xrange,接近字母表的最后,所以很少被熟知。
xrange是一个生成器对象,和下面Python 2.3的代码是等效的:
def xrange(start, stop=None, step=1):
    if stop is None:
        stop = start
        start = 0
    else:
        stop = int(stop)
    start = int(start)
    step = int(step)

    while start < stop:
        yield start
        start += step
不同的是,它是用纯C语言实现的。
xrange也有局限性。特别的是,它只对整型有效;你不能使用长整型或者浮点(他们都会被转换成整型,就像上边显示的)。
但是它确实节约了大量的内存,除非你在别的什么地方存储返回的值,在一个时间点里只有一个返回值存在。于是不同点就是:当你调用range时,它会创建一个包含了很多数字(int,long或者float)的列表。所有的对象都是一次性创建的,同时他们也是同时一起存在的。这样可能在大量的列表元素下是一个痛苦。
xrange,相反,不会立即创建数字 – 只有range对象才会。数字对象只有你在使用生成器的时候才会创建,例如选好遍历它:
xrange(sys.maxint) # No loop, and no call to .next, so no numbers are instantiated
由于这个原因,这个代码立刻就会执行。如果用range来代替时,Python会被锁起来;它会忙着申请sys.maxint个对象的空间(大约21亿个数字,在普通计算机上)。最后会因为内存用完而退出。
在Python 2.2版以前,xrange对象还存在有优化选项,例如快速成员测试(i in xrange(n)) 。由于较少的使用,在2.2之后就被去掉了。
执行时绑定函数
假如你有一个函数:
class Test:
   def check(self,a,b,c):
     if a == 0:
       self.str = b*100
     else:
       self.str = c*100

 a = Test()
 def example():
   for i in xrange(0,100000):
     a.check(i,"b","c")

 import profile
 profile.run("example()")
假设这个函数在其他地方被调用了很多次。
那么,你的检查会在第一次执行之后在if语句的地方慢下来,所以你可以这样做:
class Test2:
   def check(self,a,b,c):
     self.str = b*100
     self.check = self.check_post
   def check_post(self,a,b,c):
     self.str = c*100

 a = Test2()
 def example2():
   for i in xrange(0,100000):
     a.check(i,"b","c")

 import profile
 profile.run("example2()")

这个例子也很不充分,但是如果if语句是一个相当复杂的表达式(或者其他有很多.的表达式),你也不用考虑再评估它,如果你知道只有第一次会为真时。
性能分析代码
让你程序执行的更快的第一步就是发现瓶颈的位置。去优化那些已经执行的很快的代码几乎是没用的。我使用两个模块来帮我定位代码中的热点位置:profile和trace。在下边的例子中我也会使用timeit模块,它在2.3版第一次被引入。
小贴士:请查看profiling的文档来修改下边给出的方式。
性能分析
有很多的性能分析的模块,包括Python的发行版中。用其中的某个来分析一些函数集合是非常容易的事情。假设你的主要方法是由main调用的,没有参数,你想让它在性能评估模块的控制下执行。最简单的方式是如下:
import profile
profile.run('main()')
当main返回的时候,性能评估模块会打印一个函数调用和执行时间的表。输出可以用Stats类来自动化处理。从Python 2.4开始,性能评估也可以评估内建函数的性能。
一个有关使用profile和pstats模块来进行性能评估的稍微长一点的介绍可以在这里找到(已归档):
http://web.archive.org/web/20060506162444/http://wingware.com/doc/howtos/performance-profiling-python-code
cProfile和Hotshot模块
自从Python 2.2,hotshot包已经作为profile的替代品引入到库中,虽然cProfile模块更会比hotshot更被推荐。它的底层代码是用C写的,所以,用hotshot(或者cProfile)会消耗更少的性能,这是你程序性能的一个更精确值。在发行的版本中Tools/scripts目录下仍然有一个hotshotmain.py文件,这个可以方便你在命令行下用hotshot来控制你的程序。
Trace模块
Trace模块是profile模块的副产品,我最开始写的一些粗糙的语句级的测试覆盖。自从我发布我最开始粗糙的代码它已经被其他人修改了很多遍。对于Python 2.0你可以在Tools/scripts目录下找到trace.py文件。从Python 2.3开始它就在标准的库中了(Lib目录)。你可以把它拷贝到本地的bin目录,并赋予可执行权限,然后直接执行它。在命令行下很容易使用来更正执行整个脚本:
% trace.py -t spam.py eggs
在Python 2.4中,它能够更容易运行,只要执行python -m trace就可以。
这个命令没有单独的文档,但是你可以执行“pydoc trace”来查看内联文档。
可视化性能评估结果
RunSnakeRun是一个GUI工具,由Mike Fletcher用直方图来展示cProfile得到的结果。函数和方法调用可能更具不同的标准来排序,源代码可能会显示在可视化调用数据的旁边。
使用范例:
runsnake some_profile_dump.prof
Gprof2Dot是用Python写的工具,它可以性能评估结果转换成图形并输出到png或者svg文件中。
Python 2.5下一个典型的性能会话脚本看起来会像这样(旧一点的平台,你需要使用准确的脚本来代替-m选项):
python -m cProfile -o stat.prof MYSCRIPY.PY [ARGS...]
python -m pbp.scripts.gprof2dot -f pstats -o stat.dot stat.prof
dot -ostat.png -Tpng stat.dot
PyCallGraph,pycallgraph是一个Python模块,用来给Python程序生成调用图表。它会生成一个PNG文件,显示模块的函数调用和他们和其他函数调用的关系,函数调用的次数和函数调用的耗时。
典型应用:
pycallgraph scriptname.py
PyProf2CallTree是一个用来帮助可视化cProfile模块和kcachegrind calltree analyser生成性能评估数据的。
典型应用:
python -m cProfile -o stat.prof MYSCRIPY.PY [ARGS...]
python pyprof2calltree.py -i stat.prof -k

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值