为什么Python内层函数无需声明nonlocal即可修改外层字典变量?
首先直接给出无例外的Python变量作用域判定规则,所有你遇到的行为差异都可以用这三条规则解释:
- 变量查找顺序(LEGB规则):访问某个变量名时,Python会按优先级从高到低依次查找:当前函数局部作用域(Local) → 外层嵌套函数的作用域(Enclosing) → 全局作用域(Global) → Python内置作用域(Built-in),找到第一个匹配的名字就停止。
- 局部变量判定规则:只要你在函数内部的任意位置对某个名字执行了赋值操作(包括
=、+=、-=、del等),Python会默认将该名字判定为当前函数的局部变量,整个函数范围内访问该名字都会优先查找局部作用域,除非你提前用global(声明对应名字属于全局作用域)或nonlocal(声明对应名字属于外层嵌套函数作用域)显式指定名字的所属作用域。 - 可变对象内部修改不触发局部判定:如果你只是读取某个名字的值,或者修改该名字指向的可变对象的内部内容(比如修改字典的键值、给列表追加元素),不会触发上述的局部变量绑定判定,Python会正常按LEGB规则查找该名字。
针对你遇到的三类场景的解释
1. 直接用整数exeMax报错的原因
你改写代码时,肯定在内层modified函数中写了类似if exeTime > exeMax: exeMax = exeTime的逻辑,这里你对exeMax这个名字本身做了赋值操作,触发了局部变量判定规则,Python会把exeMax认定为modified的局部变量。
你在if exeTime > exeMax这行尝试读取exeMax的值时,局部的exeMax还没有完成赋值,因此抛出UnboundLocalError。如果你添加nonlocal exeMax声明,就是主动告诉Python这个名字不属于当前局部作用域,去外层嵌套作用域查找,就不会报错了。
2. 字典storeInfo不需要nonlocal的原因
你在modified函数中从来没有对storeInfo这个名字本身做过赋值操作,你写的storeInfo["exeMax"] = exeTime是修改storeInfo指向的字典对象的内部键值,属于可变对象的内部修改,不会触发局部变量判定。
Python会正常按LEGB规则查找storeInfo,在外层decorFun的作用域找到对应的字典对象后直接使用,因此不需要加nonlocal声明。
3. 全局变量a的示例正常运行的原因
你在函数m中只对a做了读取操作,没有任何赋值逻辑,因此不会触发局部变量判定,Python会按LEGB规则在全局作用域找到a并读取它的值。
后续你修改全局作用域中a的值,再次调用m时读取到的就是更新后的a的值,因此结果会变化。如果你在m中添加一行a = 10这类赋值操作,再调用m同样会抛出UnboundLocalError。
注:很多人会误以为差异是整数(不可变对象)和字典(可变对象)的行为不同,实际上本质原因是你对变量名本身的操作类型不同,和对象是否可变没有直接关系,上述三条规则可以覆盖所有Python作用域相关的场景,没有例外。
内容的提问来源于stack exchange,提问作者Pascal

