(重定向自 Develop.PythonHint)
On this page... (hide)
- 1. 易犯错误
- 1.1 避免混用空格和Tab
- 1.2 避免and or语法出错
- 1.3 避免用列表给函数置默认值
- 1.4 小心list的+=操作
- 2. 好用语法糖
- 2.1 在列表中使用for if子句
- 3. 常见问题
- 3.1 import特定目录下的脚本
- 3.2 Unicode与字符串编码
- 4. 经验技巧
1. 易犯错误
1.1 避免混用空格和Tab
这是最初级的原则了。Python解释器会在执行代码时将Tab当作数个空格来处理,但是到底当成几个,其实没有非常严格的保证。因此如果混用空格和Tab,就有可能使缩进产生问题,造成程序的逻辑混乱。为了代码稳定并且一致,还是建议统一使用空格。并且绝大多数现代的编辑器是能够按照使用者的设置自动将Tab转换为空格存储的,并不会因为使用空格而使编程更麻烦。
1.2 避免and or语法出错
“and or”语法在一些情况下可以替代if语句,使语法更简洁清晰。其含义是这样的:“判断条件 and 条件为真时的结果 or 条件为假时的结果”,例如“x = len(l) > 0 and l[0] or None”。但这个语法本质是利用了Python的逻辑算符的运算特征来实现,因而存在一定的使用限制,处理不好就有可能出错。
这里的主要问题是“条件为真时的结果”不能取值为假,否则即使条件为真也不会返回这个值。在Python里除了False符号之外,如None、整数0、空的list等等都会被当作逻辑假值来处理。比如“x = len(l) <= 0 and None or l[0]”这种写法,由于None被当作逻辑假值来处理,所以无论len(l)的取值为何,最后总是会执行l[0]的,这显然与该语句原先的预期效果不同。
“and or”语法的另一个可能的问题是and后面的两个子句会被执行还是被跳过其实不是很明确,需要仔细查阅Python文档看是否提供了严格的保证。
如果希望避免这种出错可能,那么也可以干脆换用另外一种表达方法:“if_true if condition else if_false”,这个表达式在condition为逻辑真值时执行并返回if_true的值,condition为逻辑假值时返回if_false的值。
1.3 避免用列表给函数置默认值
如果把列表当作某个函数的默认参数值,那么每次调用这个函数的时候这一默认参数都会指向同一个列表实例,如果这个列表实例又作为函数返回值的一部分,有时候会出问题。比如下面这个例子:
def __init__(self, data = []):
self.data = data
foo1 = Foo()
foo2 = Foo()
那么所有不指定data参数得到的Foo实例,其self.data属性都会指向同一个列表实例。也就是说foo1.data和foo2.data会指向同一个列表,一个变的时候,另一个也会跟着变。为解决这个问题,正确的写法是这样的:
def __init__(self, data = None):
self.data = data or []
1.4 小心list的+=操作
list的+=操作是存在一定歧义的。一般我们会认为a += b和a = a + b是等价的,但实际上不总是这么回事,至少对list来说不完全是一回事。我们会发现a += b会修改到a所原来指向的列表实例,也就是说,其实这和a.extend(b)是等价的。而a = a + b则不修改a所原来指向的列表实例,而只是利用a和b的内容组合出一个新的列表实例以名字a记录下来罢了。如果这个描述不够清楚,试着运行下面代码就清楚了:
b = range(5)
a_orig = a
a += b
print a
print a_orig # a_orig在以上处理过程中取值被改变了。
a = range(10)
b = range(5)
a_orig = a
a = a + b
print a
print a_orig # a_orig还是原来的值,没有变。
2. 好用语法糖
2.1 在列表中使用for if子句
Python允许在创建列表的过程中套用for if子句来构建更方便的表达方式,称为list comprehension,比如:
[x**2 for x in range(10)]
# 把列表中,大于3的元素,乘以2
vec = [2, 4, 6]
[2*x for x in vec if x > 3] # 得到[8, 12]
一种很重要的用法是初始化多维列表,正确的做法是这样:
p1 = [1] * 100
# 二维
p2 = [[1] * 100 for i in xrange(100)]
# 三维
p3 = [[[1] * 100 for i in xrange(100)] for j in xrange(100)]
仅仅使用
生成器表达式
有时我们会看到下面这样看起来很像的表达方式:
3. 常见问题
3.1 import特定目录下的脚本
Python里的import指令有自己的目录查询顺序,那么如果我们想引入特定目录下的一个脚本文件,比如上一层目录中的一段代码时,该怎么实现呢?可以像下面这样:
>>> sys.path.append('..')
>>> import test
3.2 Unicode与字符串编码
Python 2.x中字符编码的处理也是经常会碰到的,据说Python 3.0将会对这一点进行改进。在2.x中,其实str字符串是对C语言char的简单封装,因此通常就是ascii,如果其中出现多字节字符,那么Python会使用当前设置的默认编码来进行解释,中文Windows下一般是GBK或者GB18030,中文Mac下则通常是UTF-8,如果猜错了呢,Python就会抛出UnicodeDecodeError等异常。在大约2.4以前的版本中,是可以使用sys.setdefaultencode方法来指定Python运行环境的默认编码,但后来的版本将这个方法设置为默认不可见,除非显式重载sys模块,并且这个方法也不推荐使用。
在涉及非ascii编码数据时,比如抓取网页或者从文件系统读取数据就很可能出现这种情况,通常建议使用str字符串的decode方法将数据显式转换为unicode,在处理完成准备进行存储时,则根据当前操作系统等的需要调用unicode类型的encode方法转换为需要的目标编码进行存储。如果无法确定应该使用什么样的编码来进行处理,也可以考虑捕获UnicodeDecodeError异常来逐个猜测可用的编码。
Python默认的文件IO界面的open函数其实是把数据当作ascii来进行处理的,如果要指定编码完成文件IO操作,则可以使用codecs模块的open方法来替代。
4. 经验技巧
4.1 利用Queue模块完成多线程编程
使用 Python 进行线程编程这篇文章介绍了一种比较简单地实现Python多线程编程的方法,并且这种用法被认为是Python线程编程的最佳实践之一。其本质是利用了线程安全的Queue模块来作为线程之间数据沟通的界面,从而简化了很多用于处理死锁的代码。在实际使用时,既可以在代码开头将所有待处理数据一股脑扔进队列里供线程池逐个处理,也可以对线程进行分工,有的为队列生产数据,有的负责消耗队列中的数据。参考Queue模块的源代码,可以仿照该模块的接口进行扩展。
类似地,可以利用Queue模块配合Python processing模块实现比较简单的Python多进程模型开发。用进程代替线程,虽然增加了资源占用,但是能够绕过Python Gil机制的限制,更有效地利用多CPU的运算能力。
但是这里有一个问题没有搞明白,除了利用多CPU计算能力的考虑外,在什么情况下应该使用进程,又是什么情况下适合使用线程呢。所以我做了一个实验来比较Python不同的并发实现方案的性能,见Python几种并发实现方案的性能比较。
4.2 给耗时操作增加统一的TimeOut超时处理机制
无论是否启用了Python的多线程机制,只要利用signal模块就可以为耗时操作增加统一的超时处理机制(当然在使用了多线程的情况下还是有一些不一样的地方,只有在主线程里面才可以调用signal.signal函数,而子线程可以调用signal.alarm函数对信号的状态进行设置,具体需参照signal模块自身文档)。单线程情况下,可直接参考如下示例:
def handler(signum, frame):
print 'Signal handler called with signal', signum
raise TimeOutError, "TimeOut!"
try:
# Set the signal handler and a 1-second alarm
signal.signal(signal.SIGALRM, handler)
signal.alarm(1)
# This while loop hang indefinitely
while True:
print 'a',
signal.alarm(0) # Disable the alarm
except:
print 'Time out caught!'