7.1 错误使用表达式作为函数参数的默认值
你已经学到了很多东西,让我们看看新手们最常见的错误吧——最好从别人的错误中学习,而不是自己的对吧?
Python 允许通过为函数参数设置默认值来声明可选参数。这当然是语言的一个很便利的特性,但如果默认值的类型是可变的,可能会导致不愉快的后果。例如,考虑以下的函数定义:
def foo(bar=[]): # bar 是一个可选参数
# 默认值是一个空列表
bar.append("baz") # 这一行可能会成为个问题...
return bar
当下常见的错误是认为每次调用函数而不提供该参数的值时,默认参数的值都会被重置为默认值。
在上面的代码中,比如说,你可能会认为多次调用 foo()(即不为参数 bar 提供值)时,它总是会返回 ["baz"],因为你可能认为每次调用 foo()(不提供参数 bar)时,bar 都被设置为 [](即一个新的空列表)。
但让我们看看实际会发生什么:
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz", "baz"]
result = foo()
print(result) # ["baz", "baz", "baz"]
为什么函数每次调用 foo() 时都继续将值 baz 添加到已有列表中,而不是每次创建一个新列表呢?
对这个问题的答案是对 Python "底层" 机制的更深了解。具体说来,函数的默认值只在函数定义的时候被初始化一次。
因此,参数 bar 仅在第一次定义 foo() 时被初始化为默认值(即空列表),而后续的 foo() 调用(即不提供参数 bar)会继续使用首次定义函数时为参数 bar 创建的同一个列表。
顺便说一下,常见的“解决方法”如下:
def foo(bar=None):
if bar is None:
bar = []
bar.append("baz")
return bar
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
result = foo()
print(result) # ["baz"]
7.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 赋个值:
B.x = 2
print(A.x, B.x, C.x)
# 1 2 1
一切如预期。
现在我们改变类 A 的变量:
A.x = 3
print(A.x, B.x, C.x)
# 3 2 3
奇怪的是,我们只是改变了 A.x。为什么 C.x 也改变了?
在 Python 中,类变量被处理为字典,并遵循常称为方法解析顺序 (MRO) 的规则。因此,在上面的代码中,由于属性 x 没有在类 C 中找到,它会在其基类中找到(在上面的例子中只有 A,尽管 Python 支持多重继承)。
换句话说,C 没有自己独立于 A 的属性 x。因此,对 C.x 的引用实际上是对 A.x 的引用。如果没有正确考虑这些情况,这可能会引起问题。在学习 Python 时,请特别注意类属性及其操作。
7.3 错误指定异常块的参数
假设你有以下代码段:
try:
l = ["a", "b"]
int(l[2])
except ValueError, IndexError: # 是不是可以捕获这两个异常?
pass
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
IndexError: list index out of range
问题在于 except 表达式不接受这样指定的异常列表。因此在上面的代码中,IndexError 异常没有被 except 表达式捕获;相反,异常会被绑定到名为 IndexError 的参数。
重要! 你可能会在网上的例子中看到这样的代码,因为它在 Python 2.x 中曾被使用。
使用 except 表达式捕获多个异常的正确方法是将第一个参数指定为一个元组,包含所有需要捕获的异常。此外,为了更好的可读性,你可以使用 as 关键字,比如这样:
try:
l = ["a", "b"]
int(l[2])
except (ValueError, IndexError) as e:
pass
GO TO FULL VERSION