2.16. 作用域¶
当 Python 在函数内查找一个名称时,它会按特定的作用域序列进行搜索。理解这个序列可以解释为什么有些赋值会遮蔽外层名称,为什么另一些会修改它们,以及为什么嵌套函数能记住它们被定义时所在位置的值。
名称查找从局部函数作用域开始,向外走到模块作用域和内置作用域,直到找到匹配为止。¶
2.16.1. 局部作用域与模块作用域¶
在函数内部定义的名称对该函数是局部的,并在调用结束时消失:
def f():
x = 10
print(x)
f()
print(x) # NameError: x is not defined
在 .py 文件顶层定义的名称是模块级的(有时称为全局的),在该文件中的任何地方都可见,包括函数内部:
CAMERA = "OpenMV"
def banner():
print("running on", CAMERA)
从函数内部读取一个模块级名称是自动的。但从函数内部赋值给该名称则不是——该赋值会创建一个新的局部变量,在本次调用的剩余过程中遮蔽掉模块级的那个:
counter = 0
def bump():
counter = counter + 1 # UnboundLocalError
左侧的 counter 使得 counter 在 bump 中成为一个局部名称,因此右侧的读取找不到任何值。
2.16.1.1. global 关键字¶
要真正从函数内部重新赋值一个模块级名称,必须先用 global 声明它:
counter = 0
def bump():
global counter
counter += 1
应谨慎使用 global。修改隐藏状态的函数比那些将值作为参数传入、再将新值返回的函数更难推理。对于"我需要共享状态"的常见解决方法是,将一个对象(列表、字典、类实例)作为参数传入,并改为修改它。
2.16.2. Lambda¶
lambda 在单个表达式中构建一个小型匿名函数:
square = lambda x: x * x
square(7) # 49
它与下面完全等价
def square(x):
return x * x
lambda 的函数体必须是单个表达式——不能有语句,不能多行。其主要用途是将一个小函数作为参数传递给某个接受函数的对象:
pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]
当函数体超出一个表达式时,应改用真正的 def。用 def 命名函数还会在回溯中赋予它一个名称,而 lambda 没有。
2.16.3. 闭包¶
在另一个函数内部定义的函数可以读取外层函数作用域中的名称。内层函数会捕获这些名称,即使外层调用已经返回,它仍能继续工作:
def make_adder(n):
def add(x):
return x + n
return add
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))
输出:
105 110
add5 和 add10 是两个独立的函数,各自记住自己的 n。一个像这样构建并返回定制化内层函数的函数被称为闭包。这正是一门语言首先需要嵌套函数的主要原因——一种将某些状态烘焙进函数值、然后将该值作为单个可调用对象传递出去的方法。
读取被捕获的名称是自动发生的。而重新绑定某个被捕获的名称则需要一个额外的关键字。下面的示例做了看似正确的事,却失败了:
def make_counter():
count = 0
def tick():
count = count + 1 # UnboundLocalError
return count
return tick
在 tick 内部对 count 的赋值使得 count 在 tick 中成为局部的,方式与它在顶层函数中会成为局部的完全相同。nonlocal 关键字告诉 Python"这个名称存在于外层函数中,在那里重新绑定它":
def make_counter():
count = 0
def tick():
nonlocal count
count += 1
return count
return tick
c = make_counter()
print(c(), c(), c())
输出:
1 2 3
nonlocal 之于外层函数作用域,正如 global 之于模块作用域。请注意,修改一个被捕获的对象(调用 some_list.append(...)、some_dict[k] = v)不需要 nonlocal——因为名称没有被重新绑定,只是它所指向的对象被改变了。