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 使得 counterbump 中成为一个局部名称,因此右侧的读取找不到任何值。

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

add5add10 是两个独立的函数,各自记住自己的 n。一个像这样构建并返回定制化内层函数的函数被称为闭包。这正是一门语言首先需要嵌套函数的主要原因——一种将某些状态烘焙进函数值、然后将该值作为单个可调用对象传递出去的方法。

读取被捕获的名称是自动发生的。而重新绑定某个被捕获的名称则需要一个额外的关键字。下面的示例做了看似正确的事,却失败了:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

tick 内部对 count 的赋值使得 counttick 中成为局部的,方式与它在顶层函数中会成为局部的完全相同。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——因为名称没有被重新绑定,只是它所指向的对象被改变了。