函数的作用域规则

系统每次执行一个函数时,就会创建新的局部命名空间。该命名空间代表一个局部环境,其中包含函数参数的名称和在函数体内赋值的变量名称。解析这些名称时,解释器将首先搜索局部命名空间。如果没有找到匹配的名称,它就会搜索全局命名空间。函数的全局命名空间始终是定义该函数的模块。如果解释器在全局命名空间中也找不到匹配值,最终会检查内置命名空间。如果仍然找不到,就会引发 NameError 异常。

命名空间的一个特别之处,是在函数中对全局变量的操作。例如,请看以下代码:

a = 42
def foo():
    a = 13
foo()
# a仍然是42

执行这段代码时,尽管看上去我们在函数 foo 中修改了变量 a 的值,但 a 的返回值仍然是 42。当变量在函数中被赋值时,这些变量始终被绑定到该函数的局部命名空间中,因此函数体中的变量 a 引用的是一个包含值 13 的全新对象,而不是外面的变量。使用 global 语句可以改变这种行为。global 语句明确地将变量名称声明为属于全局命名空间,只有在需要修改全局变量时才必须使用它。这条语句可以放在函数体中的任意位置,并可重复使用。例如:

a = 42
b = 37
def foo():
    global a # 'a'位于全局命名空间中
    a = 13
    b = 0
foo()
# a现在已变为13。b仍然为37

Python 支持嵌套的函数定义,例如:

def countdown(start):
    n = start
    def display():  # 嵌套的函数定义
        print('T-minus %d' % n)
    while n > 0:
        display()
        n -= 1

嵌套函数中的变量是由静态作用域限定的。也就是说,解释器在解析名称时首先检查局部作用域,然后由内而外一层层检查外部嵌套函数定义的作用域。如果找不到匹配,那么和之前一样,将搜索全局命名空间和内置命名空间。尽管闭合作用域中的名称能被访问到,Python 2 只支持在最里层的作用域(即局部变量)和全局命名空间(使用 global)中给变量重新赋值。因此,内部函数不能给定义在外部函数中的局部变量重新赋值。例如,下面这段代码是不起作用的:

def countdown(start):
    n = start
    def display():
        print('T-minus %d' % n)
    def decrement():
        n -= 1   # 直接报错
    while n > 0:
        display()
        decrement()

在 Python 2 中,解决这种问题的方法是把要修改的值放在列表或字典中。在 Python 3 中,可以把 n 声明为 nonlocal,如下所示:

def countdown(start):
    n = start
    def display():
        print('T-minus %d' % n)
    def decrement():
        nonlocal n    # 绑定到外部的n,注意这个外部也要在一个函数环境内(仅在Python 3中使用)
        n -= 1
    while n > 0:
        display()
        decrement()

如果使用局部变量时还没给它赋值,就会引发 UnboundLocalError 异常,下面的例子演示了可能出现该问题的情况:

i = 0
def foo():
    i = i + 1    # 导致UnboundLocalError异常
    print(i)

这个函数具体的错误原因如下,首先因为在函数内赋值,而且没有使用 global 语句,所以表达式左侧的 i 被定义为一个局部变量,表达式右侧的 i+1 会尝试在给局部变量 i 赋值之前读取它的值,并进行增加 1 的操作。尽管这个例子中存在一个全局变量 i,并且函数中不声明 global 也能完成对全局变量的操作,但在这个例子中不会给局部变量 i 提供值。函数在定义时就确定了变量是局部的还是全局的,而且在函数中不能突然改变它们的作用域。