Python是一种解释性、面向对象并具有动态语义的高级程序语言。它内建了高级的数据结构,结合了动态类型和动态绑定的优点,这使得它在快速应用开发中非常有吸引力,并且可作为脚本或胶水语言来连接现有的组件或服务。Python 支持模块和包,从而鼓励了程序的模块化和代码重用。
Python简单易学的语法可能会使Python开发者——尤其是那些编程的初学者忽视了它的一些微妙的地方并低估了这门语言的能力。
有鉴于此,本文列出了一个“10强”名单,枚举了甚至是高级Python开发人员有时也难以捕捉的错误。
1: 滥用表达式作为函数参数的默认值
Python允许为函数的参数提供默认的可选值。尽管这是语言的一大特色,但是它可能会导致一些易变默认值的混乱。例如,看一下这个Python函数的定义:
\>>> def foo(bar=\[\]):
bar.append("baz")
return bar
一个常见的错误是认为在函数每次不提供可选参数调用时可选参数将设置为默认指定值。在上面的代码中,例如,人们可能会希望反复(即不明确指定bar参数)地调用foo()时总返回’baz’,由于每次foo()调用时都假定(不设定bar参数)bar被设置为[](即一个空列表)。
\>>> foo()
\["baz"\]
\>>> foo()
\["baz", "baz"\]
\>>> foo()
\["baz", "baz", "baz"\]
耶?为什么每次foo()调用时都要把默认值"baz"追加到现有列表中而不是创建一个新的列表呢?
答案默认参数在定义时求值(比如说当你首次导入模块时)。因此,bar参数在初始化时为其默认值(即一个空列表),即foo()首次定义的时候,但当调用foo()时(即,不指定bar参数时)将继续使用bar原本已经初始化的参数。
下面是一个常见的解决方法:
\>>> def foo(bar=None):
if bar is None:
bar = \[\]
bar.append("baz")
return bar
\>>> foo()
\["baz"\]
\>>> foo()
\["baz"\]
\>>> foo()
\["baz"\]
小编补充:
另外一个常见的例子就是默认参数是一个表达式,如:
import datetime
def log(message, time=datetime.datetime.now()):
print("{0}: {1}".format(time, message))
期望的是每次记录不同的时间,然而未能如愿,记录的是同一个时间。运行如下代码:
import time
log('message 1')
time.sleep(1)
log('message 2')
time.sleep(1)
log('message 3')
time.sleep(1)
结果如下:
2017-07-25 20:53:20.225000: message 1
2017-07-25 20:53:20.225000: message 2
2017-07-25 20:53:20.225000: message 3
2: 错误地使用类变量
考虑一下下面的例子:
\>>> class A(object):
x = 1
\>>> class B(A):
pass
\>>> class C(A):
pass
\>>> print A.x, B.x, C.x
1 1 1
没有任何异常。
\>>> B.x = 2
\>>> print A.x, B.x, C.x
1 2 1
也和你想要的结果一样。
\>>> A.x = 3
\>>> print A.x, B.x, C.x
3 2 3
结果似乎有点出乎意料了。
我们只改了A.x,为什么C.x也改了?
在Python中,类变量在内部当做字典来处理,其遵循常被引用的方法解析顺序(MRO)。所以在上面的代码中,由于class C中的x属性没有找到,它会向上找它的基类(尽管Python支持多重继承,但上面的例子中只有A)。换句话说,class C中没有它自己的x属性,其独立于A。因此,C.x事实上是A.x的引用。
假设你有如下一段代码:
\>>> try:
l = \["a", "b"\]
int(l\[2\])
except ValueError, IndexError:
pass
Traceback (most recent call last):
File "", line 3, in
IndexError: list index out of range
这里的问题在于 except 语句并不接受以这种方式指定的异常列表。相反,在Python 2.x中,使用语法 except Exception, e 是将一个异常对象绑定到第二个_可选_参数(在这个例子中是 e)上,以便在后面使用。所以,在上面这个例子中,IndexError 这个异常并不是被except语句捕捉到的,而是被绑定到一个名叫 IndexError的参数上时引发的。
在一个except语句中捕获多个异常的正确做法是将第一个参数指定为一个含有所有要捕获异常的元组。并且,为了代码的可移植性,要使用as关键词,因为Python 2 和Python 3都支持这种语法:
\>>> try:
l = \["a", "b"\]
int(l\[2\])
except (ValueError, IndexError) as e:
pass
\>>>
Python是基于 LEGB 来进行作用于解析的, LEGB是 Local, Enclosing, Global, Built-in 的缩写。看起来“见文知意”,对吗?实际上,在Python中还有一些需要注意的地方,先看下面一段代码:
\>>> x = 10
\>>> def foo():
x += 1
print x
\>>> foo()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment
这里出什么问题了?
上面的问题之所以会发生是因为当你给作用域中的一个变量赋值时,Python 会自动的把它当做是当前作用域的局部变量****,从而会隐藏外部作用域中的同名变量。
很多人会感到很吃惊,当他们给之前可以正常运行的代码的函数体的某个地方添加了一句赋值语句之后就得到了一个 UnboundLocalError 的错误。
尤其是当开发者使用列表 list 时,这个问题就更加常见. 请看下面这个例子:
\>>> lst = \[1, 2, 3\]
\>>> def foo1():
lst.append(5) \# 没有问题...
\>>> foo1()
\>>> lst
\[1, 2, 3, 5\]
\>>> lst = \[1, 2, 3\]
\>>> def foo2():
lst += \[5\] \# ... 但是这里有问题!
\>>> foo2()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment
嗯?为什么 foo2 报错,而 foo1 没有问题呢?
原因和之前那个例子的一样,不过更加令人难以捉摸。foo1 没有对 lst 进行赋值操作,而 foo2 做了。要知道, lst += [5] 是 lst = lst + [5] 的缩写,我们试图对 lst 进行赋值操作(Python把它当成了局部变量)。此外,我们对 lst 进行的赋值操作是基于 lst 自身(这再一次被Python当成了局部变量),但此时还未定义。因此出错!
下面代码中的问题应该是相当明显的:
\>>> odd = lambda x : bool(x % 2)
\>>> numbers = \[n for n in range(10)\]
\>>> for i in range(len(numbers)):
if odd(numbers\[i\]):
del numbers\[i\]
Traceback (most recent call last):
File "", line 2, in
IndexError: list index out of range
在迭代的时候,从一个列表或者数组中删除元素,对于任何有经验的开发者来说,这是一个众所周知的错误。尽管上面的例子非常明显,但是许多高级开发者在更复杂的代码中也并非是故意而为之的。
幸运的是,Python包含大量简洁优雅的编程范例,若使用得当,能大大简化和精炼代码。这样的好处是能得到更简化和更精简的代码,能更好的避免程序中出现当迭代时修改一个列表这样的bug。一个这样的范例是列表生成式(list comprehensions)。而且,列表生成式针对这个问题是特别有用的,通过更改上文中的实现,得到一段极佳的代码:
\>>> odd = lambda x : bool(x % 2)
\>>> numbers = \[n for n in range(10)\]
\>>> numbers\[:\] = \[n for n in numbers if not odd(n)\]
\>>> numbers
\[0, 2, 4, 6, 8\]
看完上半部分,这些坑你踩过吗?