python 拾遗

整理 python 使用的一些技巧,以及一些可能被忽略的细节,很多在文档可以查找到的内容将不会过多的描述,更多以外链的形式存在。

注意: 以下讨论主要为 Python2.7 版本, Python 3 的内容有待跟进

import

python 的 import 通过调用 _import_(name[, globals[, locals[, fromlist[, level]]]]) 这个函数实现,借助这个函数可以通过 python 模块的名字动态引用模块。来自tornado 的 import_object 是一个很简洁的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def import_object(name):
"""Imports an object by name.
import_object('x') is equivalent to 'import x'.
import_object('x.y.z') is equivalent to 'from x.y import z'.
"""
if name.count('.') == 0:
return __import__(name, None, None)

parts = name.split('.')
obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
try:
return getattr(obj, parts[-1])
except AttributeError:
raise ImportError("No module named %s" % parts[-1])

除此之外,我们还可以利用 python2.7 开始提供的 importlib.import_module实现代码)来进行动态引用。例如:importlib.import_module('tornado.httpclient')

reload

当 reload 一个 python 模块之后有两处不会使用新的模块的值:

  1. 原来已经使用的实例还是会使用旧的模块,而新生产的实例会使用新的模块;

  2. 其他模块引用该模块的对象,这些引用不会绑定到新的对象上。

python-reloader 是一个很有趣的项目,它将 __builtin__.__import__ 修改为自定义的 _import 函数,新的 _import 在原有调用 _import_(name[, globals[, locals[, fromlist[, level]]]]) 的同时记录下引用模块之间的依赖关系。python-reloader 实现的是 reload 一个模块之后,reload 该模块所依赖的所有模块,而不是 reload 所有依赖该模块的模块。说起来很绕,这里 issue(It should reload dependants instead of dependencies)有很好的讨论。在实际运行的系统中动态 reload 模块可能并非一种很好的选择。

Catching an exception while using ‘with’

当我们使用 with 表达式并且需要捕捉异常的时候,我们可以这样做:

1
2
3
4
5
try:
with open( "foo.txt" ) as f :
print f.readlines()
except EnvironmentError: # parent of IOError, OSError
print 'oops'

如果希望捕捉 with 表达式的异常与内部工作代码的异常分离出来,我们可以这样做:

1
2
3
4
5
6
7
try:
f = open('foo.txt')
except IOError:
print 'oops'
else:
with f:
print f.readlines()

关于 python 使用 try-except-else, with 的一些讨论:Is it a good practice to use try-except-elseUsing python “with” statement with try-except block

StringIO

当我们使用一些接收参数是文件类型的 API 时,我们可能需要使用到 StringIO,例如使用 gzip 模块压缩一个字符串:

1
2
3
4
5
6
7
8
import gzip, StringIO

stringio = StringIO.StringIO()
gzip_file = gzip.GzipFile(fileobj=stringio, mode='w')
gzip_file.write('Hello World')
gzip_file.close()

stringio.getvalue()

在 Python2.7 中 cStringIO 提供了与 StringIO 类似的接口,并且运行效率更高。在 Python3.4 中这两者被统一成为了 io.StringIO。

注意:cStringIO 的使用有一些限制:cStringIO 不能作为基类被继承;cStringIO 不能接收非 ASCII 字符的字符串参数;还有一点与 StringIO 不同的是当使用字符串参数初始化一个 cStringIO 对象时,该对象是只读的。

Queue.Queue vs collections.deque

Queue (python3 重命名为 queue)是一个可用于多线程之间同步、交换数据的队列模块,包括 FIFO,LIFO,优先级队列三个实现。

collections.deque 是一个双端队列的数据结构,在头和尾的插入、删除、读取操作是O(1)复杂度;在队列中部的随机读取操作是O(n)的。

reference:Python: Queue.Queue vs. collections.deque

heapq

python 内置的 heapq 是一个小顶堆,并且 heapify, heappush, heappop 操作是不支持传递 key 参数的。如果想实现大顶堆,可以这样 lambda x: -x,或者自己封装一层。例如:
python topN max heap。另外,heapq.nlargestheapq.nsmallest 支持 key 参数。
邮件列表里的讨论:为什么 python 的 heapq 没有支持 key 参数

itertools.tee

itertools.tee 从一个迭代器返回 n 个独立的迭代器,原始迭代器将不允许被使用,如果使用,那么可能会导致新的迭代器失效。Inside Python’s itertools.tee 很详细的探究了 itertools.tee 的实现细节。

when should we use operator

最常用的就是 operator.itemgetter,例如我们有一个 tuple 列表,需要对这些元组按照第i个元素排序,那么可以这样:lst.sort(key=operator.itemgetter(i))

operator.addlambda x, y: x+y 具有相同的效果,二者的不同主要有两个方面:一方面是它们的可读性、开发者的使用习惯的差别;另一方面是性能差别。在python wiki 中有这样一段话:

Likewise, the builtin functions run faster than hand-built equivalents. For example, map(operator.add, v1, v2) is faster than map(lambda x,y: x+y, v1, v2).

我们对二者做一次简单的实验对比:

1
2
3
4
5
6
7
8
9
10
11
➜ python -m timeit 'import operator' 'map(operator.add, [x for x in range(5000)], [y for y in range(5000)])'
1000 loops, best of 3: 710 usec per loop

➜ python3 -m timeit 'import operator' 'map(operator.add, [x for x in range(5000)], [y for y in range(5000)])'
1000 loops, best of 3: 401 usec per loop

➜ python -m timeit 'map(lambda x,y:x+y, [x for x in range(5000)], [y for y in range(5000)])'
100 loops, best of 3: 929 usec per loop

➜ python3 -m timeit 'map(lambda x,y:x+y, [x for x in range(5000)], [y for y in range(5000)])'
1000 loops, best of 3: 397 usec per loop

实验结果中,在 Python2.7 环境下 operator.add 稍微快于使用 lambda 表达式,在 Python3 环境下两者几乎没有差别。事实上 Python 并不适合 CPU 密集型的应用场景,当 CPU 不是性能瓶颈时,operator 和 lambda 之间的性能差距基本可以忽略。