「Python 进阶之路」系列 Day32
写在前面
Day04 讲闭包与作用域时提过 nonlocal,今天作为面试高频题串讲的第二篇,把 global/nonlocal 单独拎出来深挖------重点讲一个经常让人摸不着头脑的报错:明明代码是"先读取、后赋值",为什么 Python 会说这个变量"还没被赋值"?
一、是什么:两个改写外层变量的关键字
global:声明函数内某个名字指向模块级别的全局作用域,赋值会直接修改全局变量,而不是创建局部变量nonlocal:声明函数内某个名字指向最近一层外层函数的作用域(不是全局),常用于嵌套函数里修改闭包变量
python
counter = 0
def increment():
global counter
counter += 1
increment()
increment()
print(counter) # 2
二、为什么:编译期就决定了变量归属
Python 编译函数体时,会静态扫描整个函数内部------只要函数体内任意位置对某个名字有赋值动作,这个名字在整个函数体内都会被当成局部变量,不管这行赋值实际会不会被执行到、也不管它出现在读取语句之前还是之后。
global/nonlocal 就是用来打破这条默认规则的:显式声明"这个名字不要当局部变量,去外层/全局作用域找"。
三、怎么用
1. UnboundLocalError经典陷阱
不加 global,函数内对同名变量赋值只会创建一个新的局部变量,完全不影响外部:
python
x = 100
def modify_x():
x = 200
print("函数内部x:", x)
modify_x()
print("函数外部x:", x) # 100,没被修改
更容易踩的坑是:只要函数体内某处有赋值语句,哪怕赋值语句写在读取之后,Python 也会在编译期把这个名字整体标记为局部变量:
python
y = 100
def bad_read():
print(y) # 想读全局变量y
y = 200 # 但这行赋值让Python在编译期就把y标记成了局部变量
bad_read()
# UnboundLocalError: cannot access local variable 'y' where it is not associated with a value
print(y) 这一行看起来应该能读到全局的 y(毕竟这行代码执行时,下面那行赋值还没跑到),但 Python 判断"y 是不是局部变量"是整个函数体一次性静态扫描的结果,不是按代码执行顺序动态判断的------只要函数体内有 y = ...,整个函数体从头到尾的 y 都算局部变量,print(y) 这行读取的是"尚未被赋值的局部变量",直接报错。
2. 关键区别:global能自动创建,nonlocal不行
global 声明的变量即使全局作用域里一开始压根不存在,也完全没问题,调用函数时会自动创建:
python
def create_new_global():
global brand_new_var
brand_new_var = "我是运行时被global创建出来的"
print("brand_new_var" in globals()) # False
create_new_global()
print("brand_new_var" in globals()) # True
nonlocal 则要求"最近一层外层函数作用域"必须已经存在这个变量,否则在编译期直接报 SyntaxError,不会等到运行时才发现:
python
def outer2():
def inner2():
nonlocal not_exist # outer2根本没有not_exist这个变量
not_exist = 1
inner2()
# SyntaxError: no binding for nonlocal 'not_exist' found
nonlocal 也不会跳过"没有该变量"的中间层,继续往更外层或全局去找------它只认"最近一层",中间层没有就直接报错,哪怕全局作用域里恰好有同名变量:
python
mid_missing = "全局的mid_missing"
def level1():
def level2(): # level2这一层没有定义mid_missing
def level3():
nonlocal mid_missing # level3声明nonlocal,但最近一层(level2)没有这个变量
mid_missing = "level3改的"
level3()
level2()
level1()
# SyntaxError: no binding for nonlocal 'mid_missing' found
只有当变量确实存在于最近一层外层函数时,nonlocal 才能正常工作:
python
def level1_ok():
mid_var = "level1定义的"
def level2_ok():
def level3_ok():
nonlocal mid_var # 一层层往外找,在level1_ok里找到了
mid_var = "level3改的"
level3_ok()
level2_ok()
print(mid_var)
level1_ok() # level3改的
四、面试追问
Q1:global 和 nonlocal 分别解决什么问题?
global 让函数内的赋值直接作用于模块级全局变量;nonlocal 让嵌套函数内的赋值作用于最近一层外层函数的变量,两者都是为了打破"函数内赋值默认创建局部变量"这条默认规则。
Q2:为什么函数内明明在赋值语句之前读取变量,还是会报 UnboundLocalError?
Python 编译函数体时是整体静态扫描的:只要函数体内任意位置对某个名字有赋值,这个名字在整个函数体内都被当成局部变量,与赋值语句实际执行到没到、出现在读取语句前后完全无关。这个判定发生在编译期,而不是按代码实际执行顺序动态决定的。
Q3:global 和 nonlocal 在"变量不存在"时的表现有什么关键区别?
global 声明的变量哪怕全局作用域里原本不存在,调用时也能正常运行并自动创建;nonlocal 要求最近一层外层函数作用域必须已经存在这个变量,否则在编译期直接抛出 SyntaxError,不会等到运行时才暴露问题。
Q4:nonlocal 会不会跳过没有该变量的中间层,去更外层或全局找?
不会。nonlocal 只认"最近一层外层函数作用域",这一层没有就直接报错,哪怕更外层或全局恰好有同名变量,也不会继续往外查找。
Q5:多个函数共享同一个 global 变量,在实际项目里有什么隐患?
会造成隐式耦合------多个函数都能修改同一份全局状态,谁改了、什么时候改的很难追踪,尤其在多线程场景下还会引入竞态条件(Day17 讲过)。实际项目里更推荐把共享状态封装进类或者显式通过参数传递,而不是依赖分散在各处的全局变量。
下一篇预告
Day33 讲装饰器连环追问------围绕带参数的装饰器、多个装饰器叠加的执行顺序这些高频细节展开。