Python 性能小贴士 (第1部分)
=================================
原文: ``_
译者: `TheLover_Z `_
你可以在 Python 解释器中输入 ``import this`` 来看看 `Python 之禅 `_ 。一些比较敏锐的读者会注意到我使用了“解释器”这个词,然后就认为 Python 只是另一个脚本语言。“它肯定慢得要死!”
大家对这句话应该没有什么疑问:Python 程序确实没有编译型语言更快或者说更有效率。即使是 Python 的拥护者也会告诉你性能方面并不是 Python 所擅长的。然而,YouTube 已经证明了 `Python 有能力处理每小时4000万视频的需求 `_ 。如果你追求速度,你需要做的是使用第三方库(C/C++)编写更有效的代码。这里有一些小贴士:
使用内置的函数
---------------------
你当然可以写一些很有效率的代码,但是你很难超越内置的函数(使用 C 编写)。
内置函数: ``input()`` , ``int()`` , ``isinstance()`` , ``iter()`` , ``open()`` , ``ord()`` , ``pow()`` , ``print()`` , ``property()`` 。它们的速度都很快。
使用 ``join()`` 来合并大量字符串
----------------------------------------
你可以使用“+”来合并字符串。但你要考虑到,在 Python 中,字符串是不可变的,所以说每个“+”操作都需要先创建一个新的字符串然后把旧的字符串的内容拷贝过去。一个很常用的做法是用 Python 的数组模块来修改单独的字符。当你处理完毕以后,使用 ``join()`` 来生成最终的字符串。
::
>>> #This is good to glue a large number of strings
>>> for chunk in input():
>>> my_string.join(chunk)
使用多重赋值来交换变量的值
---------------------------------
这样做更简洁优雅,速度更快:
::
>>> x, y = y, x
这样要慢一点:
::
>>> temp = x
>>> x = y
>>> y = temp
尽可能的使用局部变量
--------------------------
Python 检索局部变量的时候速度更快一些。也就是说,尽量避免全局变量吧。
尽量使用 ``in``
-------------------
要检查成员信息,使用 ``in`` 。它看起来干净并且速度快。
::
>>> for key in sequence:
>>> print “found”
巧用 ``import`` 来提速
-------------------------------
将 ``import`` 放在函数体内,这样只有必要的时候才会导入模块。换句话来说,有些模块并不是马上就需要被导入的,那就等一会儿再导入它们吧!比如说,你没有必要在一开始就导入一长串的模块,这可能会拖慢你程序的速度。不过这个方法并不能提高程序的整体速度。
如果你需要一个死循环,那么请使用 ``while 1``
---------------------------------------------------
有些情况下你想要一个死循环。虽然 ``while True`` 可以达到一样的效果,但是 ``while 1`` 是一个单跳转操作。这样可以提升你程序的速度。
::
>>> while 1:
>>> #do stuff, faster with while 1
>>> while True:
>>> # do stuff, slower with wile True
使用列表推导式(list comprehension)
---------------------------------------
从 Python 2.0 开始,你可以使用列表推导式来替换掉许多的 ``for`` 和 ``while`` 。列表推导式更快一点是因为它针对 Python 解释器做了优化。并且它更易读(函数式编程),在大多数情况下它可以帮你省下来一个计数变量。比如说,我们来用一行代码数一遍从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]
对于一个非常长的序列请使用 ``xrange()``
-----------------------------------------------
这个东西可以帮你省下好多好多的内存,因为它一次只载入一个元素。相对于 ``range()`` 一次性载入整个序列, ``xrange()`` 可以帮你省下许多内存。
使用 Python 生成器
---------------------------
这也可以帮你节省内存,提高性能。如果你正在加载视频,那么你可以不需要一下子全部加载完。比如说
::
>>> chunk = ( 1000 * i for i in xrange(1000))
>>> chunk
at 0x7f65d90dcaa0>
>>> chunk.next()
0
>>> chunk.next()
1000
>>> chunk.next()
2000
使用 ``itertools`` 模块
-----------------------------
这个模块对于排列组合非常有用。我们来用三行代码生成列表[1, 2, 3]的全排列:
::
>>> 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),而不是从头开始搜索。这有什么意义呢?当你对列表使用 ``insert()`` 操作的时候需要考虑一下。比如说: ``>>> list.insert(0, element)`` 当我们向列表头部添加元素的时候效率并不高,因为所有的元素都需要改变位置。你可以使用 ``list.append()`` 向列表尾部添加元素,这样效率比较高。如果你想要在双端都可以快速插入或者删除操作的话你可以考虑双向队列。双向队列速度比较快是因为在 Python 中它是由双向链表实现的。不用多说了吧?
使用字典和集合来检查成员的存在关系
--------------------------------------
Python 在检查一个元素是不是属于一个字典或者集合的时候,速度还是很快的。因为字典和集合是由哈希表实现的,查找操作的复杂度为O(1)。因此,如果你需要频繁的检查存在关系的话,使用字典或者集合吧。
::
>>> 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
使用装饰排序(Schwartzian Transform)
---------------------------------------
``list.sort()`` 非常非常的快。Python 会将列表以自然顺序返回给你。但是如果你需要以非自然顺序排序的话,比如说,你想要按照地理位置对 IP 地址进行排序。Python 支持自定义排序,比如说你可以 ``list.sort(cmp())`` ,但是这样会很慢,因为你需要调用其它的函数。如果速度对你来说很重要的话,你可以试试 Guttman-Rosler 变换,它是基于 `Schwartzian 变换的 `_ 。读读它的算法实现还是挺有趣的,然后你就可以知道为什么 ``list.sort()`` 快一点, ``list.sort(cmp())`` 要慢一点。
使用装饰器缓存结果
-------------------
“@”是 Python 装饰器的标志。你可以用装饰器来提醒自己这个结果你待会儿要用到。比如说这个例子:
::
>>> 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
我们可以用这个装饰器来生成一个斐波纳切数列:
::
>>> @memo
>>> def fib(i):
>>> if i < 2: return 1
>>> return fib(i-1) + fib(i-2)
这个想法的要点很简单:增强(即用装饰器)你的函数,让它们可以记得每个计算过的数,这样你就不需要再重新计算了。
理解 Python GIL
-------------------------
GIL 你很有必要去了解一下。因为 CPython 的内存管理并不是线程安全的。你不能简单的创造多线程然后就希望 Python 可以把它们驾驭的很好。这是因为 GIL 会阻止多个本地线程同时执行 Python 字节码。换句话来说,GIL 会将你所有的线程进行序列化(serialize)。然而,想要给你的程序提速,你可以使用线程来管理数个并行的进程,它们可以在 Python 代码之外独立的运行。
把 Python 源代码当作你的文档
------------------------------
Python 有许多模块为了速度都用 C 来实现。当性能很苛刻并且官方文档不够的时候,看看你自己的代码。你也许可以发现潜在的数据结构和算法。Python 库是个不错的地方:http://svn.python.org/view/python/trunk/Modules
结论
----------
没什么东西可以替代人的大脑。当你觉得这个主意不够好的时候,再想想有没有更好的。希望这篇文章的一些小贴士可以让你的代码获得更高的性能。如果速度还是没有得到满足的话,Python 就需要外部的帮助了:性能分析,然后使用外部代码。我们将会在第二部分来讲讲这个问题。