Python编程规范及性能优化

jopen 11年前

Python编程规范及性能优化

Ptyhon编程规范

编码

所有的 Python 脚本文件都应在文件头标上 # -*- coding:utf-8 -*- 。设置编辑器,默认保存为 utf-8 格式。

 Python编程规范及性能优化

注释

业界普遍认同 Python 的注释分为两种的概念,一种是由 # 开头的“真正的”注释,另一种是 docstrings。前者表明为何选择当前实现以及这种实现的原理和难点,后者表明如何使用这个包、模块、类、函数(方法),甚至包括使用示例和单元测试。

坚持适当注释原则。对不存在技术难点的代码坚持不注释,对存在技术难点的代码必须注释。但与注释不同,推荐对每一个包、模块、类、函数(方法)写 docstrings,除非代码一目了然,非常简单。

缩进

Python 依赖缩进来确定代码块的层次,行首空白符主要有两种:tab 和空格,但严禁两者混用。如果使用 tab 缩进,设定 tab 为 4 个空格。

公司内部推荐使用 4 个空格的 tab 进行缩进。

空格

空格在 Python 代码中是有意义的,因为 Python 的语法依赖于缩进,在行首的空格称为前导空格。在这一节不讨论前导空格相关的内容,只讨论非前导空格。非前导空格在 Python 代码中没有意义,但适当地加入非前导空格可以增进代码的可读性。

1)在二元算术、逻辑运算符前后加空格:如 a = b + c;
2)在一元前缀运算符后不加空格,如 if !flg: pass;
3)“:”用在行尾时前后皆不加空格,如分枝、循环、函数和类定义语言;用在非行尾时两端加空格,如 dict 对象的定义 d = {‘key’ : ’value’}。
4)括号(含圆括号、方括号和花括号)前后不加空格,如 do_something(arg1, arg2),而不是 do_something( arg1, arg2 );
5)逗号后面加一个空格,前面不加空格;

空行

适当的空行有利于增加代码的可读性,加空行可以参考如下几个准则:

1)在类、函数的定义间加空行;
2)在 import 不同种类的模块间加空行;
3)在函数中的逻辑段落间加空行,即把相关的代码紧凑写在一起,作为一个逻辑段落,段落间以空行分隔;

命名

一致的命名可以给开发人员减少许多麻烦,而恰如其分的命名则可以大幅提高代码的可读性,降低维护成本。

常量

常量名所有字母大写,由下划线连接各个单词,如
WHITE = 0XFFFFFF
THIS_IS_A_CONSTANT = 1

变量

变量名全部小写,由下划线连接各个单词,如
color = WHITE
this_is_a_variable = 1

不论是类成员变量还是全局变量,均不使用 m 或 g 前缀。私有类成员使用单一下划线前缀标识,多定义公开成员,少定义私有成员。

变量名不应带有类型信息,因为 Python 是动态类型语言。如iValue、names_list、dict_obj 等都是不好的命名。

函数

函数名的命名规则与变量名相同。

类名单词首字母大写,不使用下划线连接单词,也不加入 C、T 等前缀。如:

class ThisIsAClass(object):
pass

模块

模块名全部小写,对于包内使用的模块,可以加一个下划线前缀,如:
module.py
_internal_module.py

包的命名规范与模块相同。

缩写

命名应当尽量使用全拼写的单词,缩写的情况有如下两种:

1)常用的缩写,如 XML、ID等,在命名时也应只大写首字母,如:

class XmlParser(object):pass

2)命名中含有长单词,对某个单词进行缩写。这时应使用约定成俗的缩写方式,如去除元音、包含辅音的首字符等方式,例如:

function 缩写为 fn
text 缩写为 txt
object 缩写为 obj
count 缩写为 cnt
number 缩写为 num,等。

特定命名方式

主要是指 __xxx__ 形式的系统保留字命名法。项目中也可以使用这种命名,它的意义在于这种形式的变量是只读的,这种形式的类成员函数尽量不要重载。如:

class Base(object):
def __init__(self, id, parent = None):
self.__id__ = id
self.__parent__ = parent
def __message__(self, msgid):
# …略
其中 __id__、__parent__ 和 __message__ 都采用了系统保留字命名法。

语句

Import

import 语句有以下几个原则需要遵守:

1)import 的次序,先 import Python 内置模块,再 import 第三方模块,最后 import 自己开发的项目中的其它模块;这几种模块中用空行分隔开来。
2)一条 import 语句 import 一个模块。
3)当从模块中 import 多个对象且超过一行时,使用如下断行法(此语法 py2.5 以上版本才支持):
from module import (obj1, obj2, obj3, obj4,obj5, obj6)

4)不要使用 from module import *,除非是 import 常量定义模块或其它你确保不会出现命名空间冲突的模块。

赋值

对于赋值语言,主要是不要做无谓的对齐,如:
a = 1 # 这是一个行注释
variable = 2 # 另一个行注释
fn = callback_function # 还是行注释
没有必要做这种对齐,原因有两点:一是这种对齐会打乱编程时的注意力,大脑要同时处理两件事(编程和对齐);二是以后阅读和维护都很困难,因为人眼的横向视野很窄,把三个字段看成一行很困难,而且维护时要增加一个更长的变量名也会破坏对齐。直接这样写为佳:
a = 1 # 这是一个行注释
variable = 2 # 另一个行注释
fn = callback_function # 还是行注释

分支和循环

对于分枝和循环,有如下几点需要注意的:

1)不要写成一行,如:

if !flg: pass 和 for i in xrange(10): print i都不是好代码,应写成
if !flg:
pass
for i in xrange(10):
print i
注:本文档中出现写成一行的例子是因为排版的原因,不得作为编码中不断行的依据。

2)条件表达式的编写应该足够 pythonic,如以下形式的条件表达式是拙劣的:

if len(alist) != 0: do_something()
if alist != []: do_something()
if s != “”: do_something()
if var != None: do_something()
if var != False: do_something()
上面的语句应该写成:
if seq: do_somethin() # 注意,这里命名也更改了
if var: do_something()

3)用得着的时候多使用循环语句的 else 分句,以简化代码。

已有代码

对于项目中已有的代码,可能因为历史遗留原因不符合本规范,应当看作可以容忍的特例,允许存在;但不应在新的代码中延续旧的风格。

对于第三方模块,可能不符合本规范,也应看作可以容忍的特例,允许存在;但不应在新的代码中使用第三方模块的风格。

tab 与空格混用的缩进是不可容忍的,在运行项目时应使用 –t 或 –tt 选项排查这种可能性存在。出现混用的情况时,如果是公司开发的基础类库代码,应当通知类库维护人员修改;第三方模块则可以通过提交 patch 等方式敦促开发者修正问题。

已有风格

开发人员往往在加入项目之前已经形成自有的编码风格,加入项目后应以本规范为准编写代码。特别是匈牙利命名法,因为带有类型信息,并不适合 Python 编程,不应在 Python 项目中应用。

Python性能优化

阅读 Zen of Python,在Python解析器中输入 import this. 一个犀利的Python新手可能会注意到"解析"一词, 认为Python不过是另一门脚本语言. "它肯定很慢!"

毫无疑问:Python程序没有编译型语言高效快速. 甚至Python拥护者们会告诉你Python不适合这些领域. 然而,油Tube已用Python服务于每小时4千万视频的请求. 你所要做的就是编写高效的代码和需要时使用外部实现(C/C++)代码或外部第三方工具.

代码优化

代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构、优化、扩展以及文档相关的事情通常需要消耗 80% 的工作量。优化通常包含两方面的内容:减小代码的体积,提高代码的运行效率。

有很多方法可以用来缩短程序的执和时间.记住,每当执行一个Python脚本之时,就会调用一个解释器.因而为了对此做补偿,需要对代码做点工作.这与Python是一种解释性语言有很大关系,不过通过减少需加以分析的语句的数量,也会减少解释器的总开销.

顺便提及,Python解释器具有一个命令么选项(-0代表optimize--优化),使得程序以不执行某些字节操作的方式加以执行.一般来说, 该选项用于去除节字码中给出异常产生的行号的注释,并不编译doc字符串和一些其他东西.该标志不会给出太多的速度增益,而且它会使用程序难以调试.

变量

取决于如何定义,解释器花费或多或少的时间尝试计算出它们的值.Python在尝试判定变量名时利用动态作用域规则进行处理.当它在代码中找到一个 变量时,首先通过查看局部名空间字典考察该变量是不是一个局部变量.如果找到该变量,就抓取该变量的值.否则再在全局名字空间字典中进行搜索,如果需要, 还会搜索内置名字空间.因此,局部变量比其他类型变量的搜索速度要快得多,因而获得它们的值也要快得多.局部变量搜索速度快是因为它们对应于数组中的下标 操作,而全局变量搜索则对应于散列表搜索.一个良好的优化方法是:如果在函数中使用了很多全局变量,把它们的值赋给局部变量可能会有很大帮助.

模块

在一个脚本之中,只需一次导入一个外部模块即可.因此,在代码中不需要多个import语句.实际上,应该避免在程序中尝试再导入模块.根据以往的 经验, 应该把所有的import语句放在程序头的最开始部分.然而,对一个模块多次调用import不会真正造成问题,因为它只是一个字典查找.如果必须要对一 个外部模块的某些特定属性进行大量引用,开始编写代码之前,应该考虑将这些元素复制到单个变量中(当然,如果可能的话)--特另是如果引用在一个循环内部 进行.只要导入模块,解释器就查找该模块的字节编译版.如果未找到,它会自动对模块进行字节编译并生成.pyc文件.因此,当下次尝试导入此模块时,字节 编译文件就在那里.正如读者所体会的那样.pyc文件比常规.py文件执行起来快很多,因为它们在执行之前就已经经解释器解释过.这里的建议是尽量使用字 节编译模块.不论是否拥有.pyc文件Python代码都以相同的速度执行.惟一的区别是如果存在.pyc文件,启动将会有所加快.代码的实际运行速度没 有区别.

字符串

python 中的字符串对象是不可改变的,因此对任何字符串的操作如拼接,修改等都将产生一个新的字符串对象,而不是基于原字符串,因此这种持续的 copy 会在一定程度上影响 python 的性能。对字符串的优化也是改善性能的一个重要的方面,特别是在处理文本较多的情况下。

1. 在字符串连接的使用尽量使用 join() 而不是 +;

2. 当对字符串可以使用正则表达式或者内置函数来处理的时候,选择内置函数。如 str.isalpha(),str.isdigit(),str.startswith(('x', 'yz')),str.endswith(('x', 'yz'));

3. 对字符进行格式化比直接串联读取要快,因此要在字符串与其他变量连接时就使用格式化字符串.请查看下面的连接形式:

name="Andre"
print "Hello " + name
print "Hello %s" % name

显然与第一个语句相比,第二个print语句更加优化.第三行中的括号是不需要的。

循环

对循环的优化所遵循的原则是尽量减少循环过程中的计算量,有多重循环的尽量将内层的计算提到上一层。

可以在循环中优化大量事件以便它们可平稳运行.下面就是可优化操作的简短清单.在内循环中应该使用内置函数,而不是使用采用Python编写的函 数.通过使用运行列表操作的内置函数(例如map(),reduce(),filter())代替直接循环,可以把一些循环开销转移到C代码.向 map,reduce,filter传送内置函数更会使性能得以提高.具有多重循环之时,只有最内层循环值得优化.优化多重循环时,旨在减少内存分配的次 数.使最内层循环成为交互作用次数最少者应该有助于性能设计.使用局部变量会大大改善循环内部的处理时间.只要可能,在进入循环前把所有全局变量和属性搜 索复制到局部变量.如果在嵌套循环内部使用诸如range(n)之类的结构方法,则在最外层循环外部把值域分配到一个局部变量并在循环定义中使用该变量将 快速得多.

yRange=range(500) #优化1
for xItem in range(100000):
for yItem in yRange:
print xItem,yItem

这里的另一种优化是使用xrange作为循环的x,因为100000项列表是一个相当大的列表.

yRange=range(500)
for xItem in xRange(100000): #优化2
for yItem in yRange:
print xItem,yItem

函数

Python的内置函数比采用纯Python语言编写的函数执行速度要快,因为内置函数是采用C语言编写的.map(),filter()以及 reduce就是在性能上优于采用Python编写的函数的内置函数范例.还应了解,Python把函数名作为全局常数加以处理.既然如此,前面我们看到 的名字空间搜索的整个概念同样适用于函数.如果可以选择的话,使用map()函数的隐含循环代替for循环要快得多.我在这里提到的循环的执行时间在很大 程序上取决于传送了什么函数.传送Python函数没有传送内置函数(诸如在operator模块里的那些函数)那么快.

Python中函数调用代价还是很大的。在计算密集的地方,很大次数的循环体中要尽量减少函数的调用及调用层次(能inline最好inline)。

改进算法,选择合适的数据结构

一个良好的算法能够对性能起到关键作用,因此性能改进的首要点是对算法的改进。在算法的时间复杂度排序上依次是:

O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)

因此如果能够在时间复杂度上对算法进行一定的改进,对性能的提高不言而喻。

使用内建函数

你可以用Python写出高效的代码,但很难击败内建函数. 经查证. 他们非常快速.

 Python编程规范及性能优化

使用join()连接字符串

你可以使用 "+" 来连接字符串. 但由于string在Python中是不可变的,每一个"+"操作都会创建一个新的字符串并复制旧内容. 常见用法是使用Python的数组模块单个的修改字符;当完成的时候,使用 join() 函数创建最终字符串.

>>> #This is good to glue a large number of strings
>>> for chunk in input():
>>> my_string.join(chunk)

使用Python多重赋值,交换变量

在Python中即优雅又快速:
>>> x, y = y, x
这样很慢:
>>> temp = x
>>> x = y
>>> y = temp

尽量使用局部变量

Python 检索局部变量比检索全局变量快. 这意味着,避免 "global" 关键字.

尽量使用 "in"

使用 "in" 关键字. 简洁而快速.

>>> for key in sequence:
>>> print “found”

使用延迟加载加速

將 "import" 声明移入函数中,仅在需要的时候导入. 换句话说,如果某些模块不需马上使用,稍后导入他们. 例如,你不必在一开使就导入大量模块而加速程序启动. 该技术不能提高整体性能. 但它可以帮助你更均衡的分配模块的加载时间.

为无限循环使用 "while 1"

有时候在程序中你需一个无限循环.(例如一个监听套接字的实例) 尽管 "while true" 能完成同样的事, 但 "while 1" 是单步运算. 这招能提高你的Python性能.

使用 Lazy if-evaluation 的特性

Python 中条件表达式是 lazy evaluation 的,也就是说如果存在条件表达式 if x and y,在 x 为 false 的情况下 y 表达式的值将不再计算。因此可以利用该特性在一定程度上提高程序效率。

使用list comprehension和generator expression

从Python 2.0 开始,你可以使用 list comprehension 取代大量的 "for" 和 "while" 块. 使用List comprehension通常更快,Python解析器能在循环中发现它是一个可预测的模式而被优化.额外好处是,list comprehension更具可读性(函数式编程),并在大多数情况下,它可以节省一个额外的计数变量。列表解析要比在循环中重新构建一个新的 list 更为高效,因此我们可以利用这一特性来提高运行的效率。例如,让我们计算1到10之间的偶数个数:

>>> # the good way to iterate a range
>>> evens = [ i for i in range(10) if i%2 == 0]
>>> [0, 2, 4, 6, 8]
>>> # the following is not so Pythonic
>>> i = 0
>>> evens = []
>>> while i < 10:
>>> if i %2 == 0: evens.append(i)
>>> i += 1
>>> [0, 2, 4, 6, 8]

生成器表达式则是在 2.4 中引入的新内容,语法和列表解析类似,但是在大数据量处理时,生成器表达式的优势较为明显,它并不创建一个列表,只是返回一个生成器,因此效率较高。例 如:代码 a = [w for w in list] 修改为 a = (w for w in list),运行时间进一步减少,缩短约为 2.98s。

使用Dictionary comprehensions/Set comprehensions

大多数的Python程序员都知道且使用过列表推导(list comprehensions)。如果你对list comprehensions概念不是很熟悉——一个list comprehension就是一个更简短、简洁的创建一个list的方法。

>>> some_list = [1, 2, 3, 4, 5]

>>> another_list = [x + 1 for x in some_list]

>>> another_list

>>> [2, 3, 4, 5, 6]

自从python 3.1 (甚至是Python 2.7)起,我们可以用同样的语法来创建集合和字典表:

>>>#Se Comprehensions

>>> some_list = [1, 2, 3, 4, 5, 2, 5, 1, 4, 8]

>>> even_set = { x for x in some_list if x % 2 == 0 }

>>> even_set set([8, 2, 4])

>>> # Dict Comprehensions

>>> d = {x: x % 2 == 0 for x in range(1, 11)}

>>> d {1: False, 2: True, 3: False, 4: True, 5: False, 6: True, 7: False, 8: True, 9: False, 10: True}

在第一个例子里,我们以some_list为基础,创建了一个具有不重复元素的集合,而且集合里只包含偶数。而在字典表的例子里,我们创建了一个key是不重复的110之间的整数,value是布尔型,用来指示key是否是偶数。

这里另外一个值得注意的事情是集合的字面量表示法。我们可以简单的用这种方法创建一个集合:

>>> my_set ={1, 2, 1, 2, 3, 4}

>>> my_set

set([1, 2, 3, 4])

而不需要使用内置函数set()

集合 (set) 与列表 (list)

set union intersectiondifference 操作要比 list 的迭代要快。因此如果涉及到求 list 交集,并集或者差的问题可以转换为 set 来操作。

set(list1) | set(list2)

union

包含list1和list2所有数据的新集合

set(list1) & set(list2)

intersection

包含list1和list2中共同元素的新集合

set(list1) - set(list2)

difference

在list1中出现但不在list2中出现的元素的集合

使用dict 和 set 测试成员

Python dict中使用了 hash table,因此查找操作的复杂度为 O(1),因此对成员的查找访问等操作字典要比 list 更快。

检查一个元素是在dicitonary或set是否存在,这在Python中非常快的。这是因为dict和set使用哈希表来实现,查找效率可以达 到O(1),而 list 实际是个数组,在 list 中,查找需要遍历整个 list,其复杂度为 O(n),因此,如果您需要经常检查成员,使用 set 或 dict做为你的容器。

>>> mylist = ['a', 'b', 'c'] #Slower, check membership with list:
>>> ‘c’ in mylist
>>> True
>>> myset = set(['a', 'b', 'c']) # Faster, check membership with set:
>>> ‘c’ in myset:
>>> True

计数时使用Counter计数对象

这听起来显而易见,但经常被人忘记。对于大多数程序员来说,数一个东西是一项很常见的任务,而且在大多数情况下并不是很有挑战性的事情——这里有几种方法能更简单的完成这种任务。

Pythoncollections类库里有个内置的dict类的子类,是专门来干这种事情的:

>>>from collections import Counter

>>>c = Counter('hello world')

>>>c

Counter({'l': 3, 'o': 2, ' ': 1, 'e': 1, 'd': 1, 'h': 1, 'r': 1, 'w': 1})

>>>c.most_common(2)

[('l', 3), ('o', 2)]

使用xrange()处理长序列

这样可为你节省大量的系统内存,因为xrange()在序列中每次调用只产生一个整数元素。而相反 range(),它將直接给你一个完整的元素列表,用于循环时会有不必要的开销。

使用 Python generator

这也可以节省内存和提高性能。例如一个视频流,你可以一个一个字节块的发送,而不是整个流。例如:

>>> chunk = ( 1000 * i for i in xrange(1000))
>>> chunk
>>> chunk.next()
0
>>> chunk.next()
1000
>>> chunk.next()
2000

了解itertools模块

该模块对迭代和组合是非常有效的。让我们生成一个列表[1,2,3]的所有排列组合,仅需三行Python代码:

>>> import itertools
>>> iter = itertools.permutations([1,2,3])
>>> list(iter)
[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

学习bisect模块保持列表排序

这是一个免费的二分查找实现和快速插入有序序列的工具。也就是说,你可以使用:

>>> import bisect
>>> bisect.insort(list, element)
你已將一个元素插入列表中, 而你不需要再次调用 sort() 来保持容器的排序, 因为这在长序列中这会非常昂贵.

理解Python列表,实际上是一个数组

Python中的列表实现并不是以人们通常谈论的计算机科学中的普通单链表实现的。Python中的列表是一个数组。也就是说,你可以以常量时间 O(1) 检索列表的某个元素,而不需要从头开始搜索。这有什么意义呢? Python开发人员使用列表对象insert()时, 需三思.

例如:>>> list.insert(0,item)

在列表的前面插入一个元素效率不高, 因为列表中的所有后续下标不得不改变. 然而,您可以使用list.append()在列表的尾端有效添加元素. 优先选择deque,如果你想快速的在插入或删除时。它是快速的,因为在Python中的deque用双链表实现。

使用Schwartzian Transform 的 sort()

原生的list.sort()函数是非常快的。 Python会按自然顺序排序列表。有时,你需要非自然顺序的排序。例如,你要根据服务器位置排序的IP地址。 Python支持自定义的比较,你可以使用list.sort(CMP()),这会比list.sort()慢,因为增加了函数调用的开销。如果性能有问 题,你可以申请Guttman-Rosler Transform,基于Schwartzian Transform. 它只对实际的要用的算法有兴趣,它的简要工作原理是,你可以变换列表,并调用Python内置list.sort() - > 更快,而无需使用list.sort(CMP() )->慢。

 Python编程规范及性能优化

Python装饰器缓存结果

“@”符号是Python的装饰语法。它不只用于追查,锁或日志。你可以装饰一个Python函数,记住调用结果供后续使用。这种技术被称为memoization的。下面是一个例子:

>>> from functools import wraps
>>> def memo(f):
>>> cache = { }
>>> @wraps(f)
>>> def wrap(*arg):
>>> if arg not in cache: cache['arg'] = f(*arg)
>>> return cache['arg']
>>> return wrap
我们也可以对 Fibonacci 函数使用装饰器:
>>> @memo
>>> def fib(i):
>>> if i < 2: return 1
>>> return fib(i-1) + fib(i-2)

这里的关键思想是:增强函数(装饰)函数,记住每个已经计算的Fibonacci值;如果它们在缓存中,就不需要再计算了.

理解Python的GIL(全局解释器锁)

GIL是必要的,因为CPython的内存管理是非线程安全的。你不能简单地创建多个线程,并希望Python能在多核心的机器上运行得更快。这是 因为 GIL將会防止多个原生线程同时执行Python字节码。换句话说,GIL將序列化您的所有线程。然而,您可以使用线程管理多个派生进程加速程序,这些程 序独立的运行于你的Python代码外。

使用multiprocessing模块实现真正的并发

因为GIL会序列化线程, Python中的多线程不能在多核机器和集群中加速. 因此Python提供了multiprocessing模块, 可以派生额外的进程代替线程, 跳出GIL的限制. 此外, 你也可以在外部C代码中结合该建议, 使得程序更快.

注意, 进程的开销通常比线程昂贵, 因为线程自动共享内存地址空间和文件描述符. 意味着, 创建进程比创建线程会花费更多, 也可能花费更多内存. 这点在你计算使用多处理器时要牢记.

 Python编程规范及性能优化

本地代码

好了, 现在你决定为了性能使用本地代码. 在标准的ctypes模块中, 你可以直接加载已编程的二进制库(.dll 或 .so文件)到Python中, 无需担心编写C/C++代码或构建依赖. 例如, 我们可以写个程序加载libc来生成随机数。

然而, 绑定ctypes的开销是非轻量级的. 你可以认为ctypes是一个粘合操作系库函数或者硬件设备驱动的胶水. 有几个如 SWIG, Cython和Boost 此类Python直接植入的库的调用比ctypes开销要低. Python支持面向对象特性, 如类和继承. 正如我们看到的例子, 我们可以保留常规的C++代码, 稍后导入. 这里的主要工作是编写一个包装器 (行 10~18).

 Python编程规范及性能优化

像熟悉文档一样的熟悉Python源代码

Python有些模块为了性能使用C实现。当性能至关重要而官方文档不足时,可以自由探索源代码。你可以找到底层的数据结构和算法。

其他优化技巧

1. 如果需要交换两个变量的值使用 a,b=b,a 而不是借助中间变量 t=a;a=b;b=t;

>>> from timeit import Timer

>>> Timer("t=a;a=b;b=t","a=1;b=2").timeit()

0.25154118749729365

>>> Timer("a,b=b,a","a=1;b=2").timeit()

0.17156677734181258

>>>

2. 在循环的时候使用 xrange 而不是 range;使用 xrange 可以节省大量的系统内存,因为 xrange() 在序列中每次调用只产生一个整数元素。而 range() 將直接返回完整的元素列表,用于循环时会有不必要的开销。在 python3 中 xrange 不再存在,里面 range 提供一个可以遍历任意长度的范围的 iterator。

3. 使用局部变量,避免"global" 关键字。python 访问局部变量会比全局变量要快得多,因此可以利用这一特性提升性能。

4. if done is not None 比语句 if done != None 更快,读者可以自行验证;

5. 在耗时较多的循环中,可以把函数的调用改为内联的方式;

6. 使用级联比较 "x < y < z" 而不是 "x < y and y < z";

7. while 1 要比 while True 更快(当然后者的可读性更好);

8. build in 函数通常较快,add(a,b) 要优于 a+b。

结论

这些不能替代大脑思考. 打开引擎盖充分了解是开发者的职责,使得他们不会快速拼凑出一个垃圾设计. 以上的Python建议可以帮助你获得好的性能. 如果速度还不够快, Python將需要借助外力:分析和运行外部代码。

工具优化

有益的提醒,静态编译的代码仍然重要. 仅例举几例, Chrome,Firefox,MySQL,MS Office 和 Photoshop都是高度优化的软件,我们每天都在使用. Python作为解析语言,很明显不适合. 不能单靠Python来满足那些性能是首要指示的领域. 这就是为什么Python支持让你接触底层裸机基础设施的原因, 将更繁重的工作代理给更快的语言如C. 这高性能计算和嵌入式编程中是关键的功能.

首先 拒绝调优诱惑

 Python编程规范及性能优化

调优给你的代码增加复杂性. 集成其它语言之前, 请检查下面的列表. 如果你的算法是"足够好", 优化就没那么迫切了.

1. 你做了性能测试报告吗?
2. 你能减少硬盘的 I/O 访问吗?
3. 你能减少网络 I/O 访问吗?
4. 你能升级硬件吗?
5. 你是为其它开发者编译库吗?
6. 你的第三方库软件是最新版吗?

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

使用工具监控代码 而不是直觉

速度的问题可能很微妙, 所以不要依赖于直觉. 感谢 "cprofiles" 模块, 通过简单的运行你就可以监控Python代码.

1. “python -m cProfile myprogram.py”

 Python编程规范及性能优化

2. 使用import profile模块

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()")

程序的运行结果如下:

 Python编程规范及性能优化

其中输出每列的具体解释如下:

ncalls:表示函数调用的次数;

tottime:表示指定函数的总的运行时间,除掉函数中调用子函数的运行时间;

percall:(第一个 percall)等于 tottime/ncalls;

cumtime:表示该函数及其所有子函数的调用运行的时间,即函数开始调用到返回的时间;

percall:(第二个 percall)即函数运行一次的平均时间,等于 cumtime/ncalls;

filename:lineno(function):每个函数调用的具体信息;

如果需要将输出以日志的形式保存,只需要在调用的时候加入另外一个参数。如 profile.run("profileTest()","testprof")。

对于大型应用程序,如果能够将性能分析的结果以图形的方式呈现,将会非常实用和直观,常见的可视化工具有 Gprof2Dot,visualpytune,KCacheGrind 等,读者可以自行查阅相关官网。

审查时间复杂度

控制以后, 提供一个基本的算法性能分析. 恒定时间是理想值. 对数时间复度是稳定的. 阶乘复杂度很难扩展.

O(1) -> O(lg n) -> O(n lg n) -> O(n^2) -> O(n^3) -> O(n^k) -> O(k^n) -> O(n!)

使用第三方包

Python 性能优化除了改进算法,选用合适的数据结构之外,还有几种关键的技术,比如将关键 python 代码部分重写成 C 扩展模块,或者选用在性能上更为优化的解释器等,这些在本文中统称为优化工具。python 有很多自带的优化工具,如 Psyco,Pypy,Cython,Pyrex 等,这些优化工具各有千秋。

Psyco

psyco 是一个 just-in-time 的编译器,它能够在不改变源代码的情况下提高一定的性能,Psyco 将操作编译成有点优化的机器码,其操作分成三个不同的级别,有"运行时""编译时""虚拟时"变量。并根据需要提高和降低变量的级别。运行时变量只是 常规 Python 解释器处理的原始字节码和对象结构。一旦 Psyco 将操作编译成机器码,那么编译时变量就会在机器寄存器和可直接访问的内存位置中表示。同时 python 能高速缓存已编译的机器码以备今后重用,这样能节省一点时间。但 Psyco 也有其缺点,其本身运行所占内存较大。目前 psyco 已经不在 python2.7 中支持,而且不再提供维护和更新了,对其感兴趣的可以参考:

http://psyco.sourceforge.net/

http://developer.51cto.com/art/201301/376509.htm

Pypy

PyPy 表示 " Python 实现的 Python",但实际上它是使用一个称为RPython Python 子集实现的,能够将 Python 代码转成 C .NET Java 等语言和平台的代码。PyPy 集成了一种即时 (JIT) 编译器。和许多编译器,解释器不同,它不关心 Python代码的词法分析和语法树。 因为它是用 Python 语言写的,所以它直接利用 Python 语言的 Code ObjectCode Object Python 字节码的表示,也就是说, PyPy直接分析 Python 代码所对应的字节码,这些字节码即不是以字符形式也不是以某种二进制格式保存在文件中, 而在 Python 运行环境中。目前版本是 1.8. 支持不同的平台安装,windows 上安装 Pypy 需要先下载 https://bitbucket.org/pypy/pypy/downloads/pypy-1.8-win32.zip 然后解压到相关的目录,并将解压后的路径添加到环境变量 path 中即可。在命令行运行 pypy,如果出现如下错误:"没有找到 MSVCR100.dll, 因此这个应用程序未能启动,重新安装应用程序可能会修复此问题",则还需要在微软的官网上下载 VS 2010 runtime libraries 解决该问题。具体地址为 http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=5555

Cython

Cython 是用 python 实现的一种语言,可以用来写 python 扩展,用它写出来的库都可以通过 import 来载入,性能上比 python 的快。cython 里可以载入 python 扩展 ( 比如 import math),也可以载入 c 的库的头文件 ( 比如 :cdef extern from "math.h"),另外也可以用它来写 python 代码。将关键部分重写成 C 扩展模块。

以上第三方工具可参考:

http://www.linuxidc.com/Linux/2012-07/66757p3.htm

总结

希望这些Python建议能让你成为一个更好的开发者. 最后, 我需要指出, 追求性能极限是一个有趣的游戏, 而过度优化就会变成嘲弄了. 虽然Python授予你与C接口无缝集成的能力, 你必须问自己你花数小时的艰辛优化工作用户是否买帐. 另一方面, 牺牲代码的可维护性换取几毫秒的提升是否值得. 团队中的成员常常会感谢你编写了简洁的代码. 尽量贴近Python的方式, 因为人生苦短. :)