明明在赋值前读取,为什么还会报 UnboundLocalError?global 与 nonlocal 深挖

「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不行

flowchart TD A[global声明] --> B[直接绑定模块的全局作用域<br/>不存在就自动创建] C[nonlocal声明] --> D{最近一层外层函数<br/>是否已存在该变量} D -->|存在| E[绑定到那一层] D -->|不存在| F[直接SyntaxError<br/>不会继续往外找]

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:globalnonlocal 分别解决什么问题?

global 让函数内的赋值直接作用于模块级全局变量;nonlocal 让嵌套函数内的赋值作用于最近一层外层函数的变量,两者都是为了打破"函数内赋值默认创建局部变量"这条默认规则。

Q2:为什么函数内明明在赋值语句之前读取变量,还是会报 UnboundLocalError

Python 编译函数体时是整体静态扫描的:只要函数体内任意位置对某个名字有赋值,这个名字在整个函数体内都被当成局部变量,与赋值语句实际执行到没到、出现在读取语句前后完全无关。这个判定发生在编译期,而不是按代码实际执行顺序动态决定的。

Q3:globalnonlocal 在"变量不存在"时的表现有什么关键区别?

global 声明的变量哪怕全局作用域里原本不存在,调用时也能正常运行并自动创建;nonlocal 要求最近一层外层函数作用域必须已经存在这个变量,否则在编译期直接抛出 SyntaxError,不会等到运行时才暴露问题。

Q4:nonlocal 会不会跳过没有该变量的中间层,去更外层或全局找?

不会。nonlocal 只认"最近一层外层函数作用域",这一层没有就直接报错,哪怕更外层或全局恰好有同名变量,也不会继续往外查找。

Q5:多个函数共享同一个 global 变量,在实际项目里有什么隐患?

会造成隐式耦合------多个函数都能修改同一份全局状态,谁改了、什么时候改的很难追踪,尤其在多线程场景下还会引入竞态条件(Day17 讲过)。实际项目里更推荐把共享状态封装进类或者显式通过参数传递,而不是依赖分散在各处的全局变量。


下一篇预告

Day33 讲装饰器连环追问------围绕带参数的装饰器、多个装饰器叠加的执行顺序这些高频细节展开。

相关推荐
YHL1 小时前
🎯 Danci —— 用 AI 驱动开发一个全栈英语单词学习平台
前端·后端
小毛驴8501 小时前
Jupyter Notebook 安装步骤
ide·python·jupyter
步行cgn1 小时前
Spring 的核心特点与设计哲学
后端
掘金者阿豪1 小时前
数据库迁移最麻烦的,不是迁,而是动手之前心里没底
后端
BUG研究员_1 小时前
LangGraph节点缓存-CachePolicy
python·缓存·langraph
中趴菜1 小时前
HTML 双重转义排查实战指南
后端
quantdash_cc1 小时前
如何设计高效率的批量股票数据获取程序?QuantDash Python SDK 全市场行情拉取实战指南
开发语言·python·数据分析·量化交易·股票数据·quantdash
AskHarries1 小时前
花了 500 大洋买下 bbs.ss,我决定做一个真正属于出海人的论坛
后端
砚底藏山河2 小时前
拉数管道设计:从一次性脚本到可重启的数据流水线(魔码量化实战 #01)
java·数据库·python·金融·maven