Python3.8嵌套函数使用global/nonlocal变量的报错问题求解
问题核心是Python的LEGB作用域查找规则,以及global、nonlocal两个关键字的作用边界差异,以下是具体说明。
基础规则先明确
Python查找变量时严格按照LEGB顺序逐层查找:
- L(Local):当前函数/代码块的局部作用域
- E(Enclosing):外层嵌套函数的作用域,从最近一层向外查找
- G(Global):模块级别的全局作用域
- B(Built-in):Python内置作用域
两个作用域关键字的作用完全不同:
global 变量名:强制让当前作用域内的该变量直接指向全局作用域的同名变量,跳过所有外层嵌套函数的作用域。如果全局作用域不存在该变量,读取操作会直接触发NameError。nonlocal 变量名:强制让当前作用域内的该变量指向最近一层外层嵌套函数中定义的同名局部变量,不会查找全局作用域。如果外层嵌套函数链中没有定义该变量,声明阶段就会报错。
疑问1:代码块1报错、代码块2正常运行的原因
两段代码结构完全一致,运行结果差异和代码逻辑本身无关,是运行环境的变量残留导致的:
代码块1报错逻辑
你在a2中声明global res后,a2会直接去全局作用域找res,完全跳过外层函数a1_里定义的res = 0(这个res属于Enclosing层,不在全局作用域)。由于全局作用域从来没有定义过res,执行打印语句时找不到变量,触发NameError。
def a1_(r): r+=1 res = 0 print('a1_res=',id(res)) def a2(r): global res print('res = ',res) print('a2_res=',id(res)) res+=r return res return a2(r) a1_(1)
代码块2运行正常的原因
代码块2的逻辑和代码块1完全相同:foo2中声明global a后,会直接去全局作用域找a,跳过外层foo里定义的a=1。你运行时没有报错,是因为当前Python环境的全局作用域里已经提前存在名为a的变量——比如之前运行其他代码时给全局a赋过值,或者交互环境有变量残留。
def foo(): a = 1 def foo2(): global a print(a) a+=1 return a return foo2() foo()
你可以重启一个全新的Python解释器,先执行del a清除可能存在的全局变量,再运行代码块2,会触发和代码块1完全一致的NameError。
疑问2:替换为nonlocal后代码正常运行的原因
你需要访问的res本来就定义在外层函数a1_的局部作用域(Enclosing层),nonlocal res的声明刚好让内层a2中的res指向这层外层函数的变量,和变量的实际定义位置完全匹配,因此可以正常读取、修改,不会跨到全局作用域查找不存在的变量。
def a1_(r): r+=1 res = 0 print('a1_res=',id(res)) def a2(r): nonlocal res print('res = ',res) print('a2_res=',id(res)) res+=r return res return a2(r) a1_(1)
运行这段代码时你会发现,两次打印的id(res)值完全相同,说明内层和外层的res是同一个变量,符合nonlocal的作用逻辑。
实用总结
- 要读写模块级别的全局变量,用
global声明,变量需要定义在全局作用域,或者在声明为global的代码块中先赋值再读取 - 要读写外层嵌套函数的局部变量,用
nonlocal声明,变量必须定义在外层嵌套函数的作用域中 - 内层函数默认可以只读访问外层函数的变量,但只要尝试对变量做赋值操作,Python会默认将其视为当前函数的局部变量,除非显式添加
global或nonlocal声明
内容的提问来源于stack exchange,提问作者HAOSHENG SHEN

