8.1 Python 作用域规则的误解
Python 的作用域基于所谓的 LEGB 规则,这个缩写的意思是:
-
Local(在函数内部(def或lambda)任何方式分配的名字,且没有在此函数中声明为全局的); Enclosing(任何静态包含函数(def或lambda)中的本地作用域名字,从内部到外部);Global(在模块文件的顶层分配的名字,或通过在def内执行global指令);-
Built-in(在内置名字模块中预先分配的名字:open,range,SyntaxError,等等)。
听起来很简单,对吧?
但是在 Python 中,这里面有一些细微之处,这让我们面临 Python 编程中的复杂问题。看下面的例子:
x = 10
def foo():
x += 1
print(x)
foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment
问题出在哪儿?
上面的错误是因为,当你在作用域中赋值给变量时,Python 会自动认为它是这个作用域的局部变量,并且会隐藏在任何高一级作用域中的同名变量。
因此,很多人在他们的代码中加入一个赋值操作后,感到惊讶地收到了 UnboundLocalError 。
这种特性在使用列表时尤其让开发者迷惑。看看下面的例子:
lst = [1, 2, 3]
def foo1():
lst.append(5) # 这个没问题...
foo1()
print(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 本身(再次,现在假设它在局部作用域中),而这还没有被定义。所以我们得到一个错误。
8.2 在迭代列表时修改它
下面这段代码中的问题应该很明显:
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 中一个众所周知的问题,任何有经验的软件开发人员都很熟悉这个问题。但尽管上面的例子可能很明显,甚至经验丰富的开发人员在更复杂的代码中也可能踩到这个坑。
幸运的是,Python 包含了一系列优雅的编程范式,当正确使用时,可以显著简化和优化代码。另一个令人愉快的后果是,在更简单的代码中,意外删除迭代列表元素的错误的可能性要小得多。
其中一个范式是列表生成器。此外,理解列表生成器的工作原理特别有助于避免这个特定问题,如下所示的替代代码实现就能很好地工作:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # 只是选择新的元素
print(numbers)
# [0, 2, 4, 6, 8]
重要! 这里并没有分配新的列表对象。使用numbers[:] 是批量 分配新值给列表的所有元素。
8.3 不理解 Python 如何在闭包中绑定变量
看下面的例子:
def create_multipliers():
return [lambda x: i * x for i in range(5)] #返回函数列表!
for multiplier in create_multipliers():
print(multiplier(2))
你可能期待如下输出:
0
2
4
6
8
但实际上你会得到:
8
8
8
8
8
惊不惊喜?
这是因为 Python 的晚绑定,这意味着在闭包中使用的变量的值会在内部函数调用时才搜索。
因此在上面的代码中,每当调用任何一个返回的函数时,i 的值会在调用时在周围作用域中查找(那时循环已经完成,所以 i 被赋予了最终结果—4)。
解决这个 Python 常见问题的方法是:
def create_multipliers():
return [lambda x, i = i : i * x for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 0 # 2 # 4 # 6 # 8
瞧!我们在这里使用默认参数来为匿名函数生成达到我们期望的行为。有人会称这是优雅的解决方案。有人会觉得微妙。有人讨厌这样的东西。但如果你是一个 Python 开发者,这种理解是至关重要的。
GO TO FULL VERSION