码农高天
Python在编译期依据函数内是否存在赋值操作强行绑定局部作用域,导致未加global或nonlocal声明的代码无论书写先后都会在运行时抛出未定义错误。
AI-assisted summary of 【python】定义好的变量读不出来?详解全局变量和自由变量的使用! — original source. Check the original for full context.
Source published:
Key ideas
- Python 作用域判定发生在编译阶段,只要函数内存在对某变量的赋值操作,该函数内对该变量的所有引用都会被默认标记为局部变量(使用 LOAD_FAST 指令),即便读取语句写在赋值语句之前也会触发 UnboundLocalError。 (0:00)
- 在嵌套函数闭包中若要修改外层函数的自由变量,必须显式使用 nonlocal 声明;若使用 global 会错误指向顶层全局命名空间,而若不加声明则会被视为内层函数的独立局部变量。 (3:00)
- 民间常说的“全局变量可读不可写”容易误导开发者;判定是否需要 global 声明的唯一标准是变量符号本身是否被赋值绑定(如出现在等号左边),修改容器内部状态(如 d['k'] = v)本质上只是读取变量后操作内部内容,无需 global。 (3:00)
Detailed notes
## Background 该视频由“码农高天”于2024年5月12日发布在YouTube平台上。视频简介指出,本期内容旨在深入剖析Python中使用全局变量和自由变量时常踩的坑,并详细解答何时需要显式添加global与nonlocal声明。
## TL;DR 视频从Python中全局变量在函数内因赋值而导致读取崩溃(UnboundLocalError)的反直觉现象切入,通过对比字节码揭示了Python在编译阶段判定变量作用域的底层机制。作者指出,只要函数内部存在对变量的赋值操作,无论赋值语句写在第几行,Python编译期都会默认将其标记为局部变量并生成LOAD_FAST指令。对于闭包结构中的自由变量,则需使用nonlocal而非global进行显式声明。最后,作者澄清了“可读不可写”这一常见误区的本质,强调必须严格观察变量符号本身是否出现在等号左边被重新绑定,而非单纯看对象内容是否被原地修改。
## Atmosphere & style 视频展现了硬核而又接地气的技术拆解风格,主讲人以开发者日常极易踩坑的困惑为引子,直奔底层逻辑。讲解语调平静沉稳、条理清晰,通过对比字节码和代码执行路径,抽丝剥茧地还原出Python编译器的运作真相。整体氛围既有直击语言设计“反人类”痛点的共鸣感,又有看清底层运行机制后豁然开朗的解惑快感。
## Bulletin - [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) Python 作用域判定发生在编译阶段,只要函数内存在对某变量的赋值操作,该函数内对该变量的所有引用都会被默认标记为局部变量(使用 LOAD_FAST 指令),即便读取语句写在赋值语句之前也会触发 UnboundLocalError。 - [03:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=180s) 在嵌套函数闭包中若要修改外层函数的自由变量,必须显式使用 nonlocal 声明;若使用 global 会错误指向顶层全局命名空间,而若不加声明则会被视为内层函数的独立局部变量。 - [03:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=180s) 民间常说的“全局变量可读不可写”容易误导开发者;判定是否需要 global 声明的唯一标准是变量符号本身是否被赋值绑定(如出现在等号左边),修改容器内部状态(如 d['k'] = v)本质上只是读取变量后操作内部内容,无需 global。
## Key takeaways - [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 在函数内仅读取全局变量时程序可正常运行,但一旦加入修改该变量的赋值语句,即便读取放在赋值之前也会抛出 UnboundLocalError 报错。 - [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 通过对比 Python 字节码可以发现,纯读取时使用的是 LOAD_GLOBAL 指令,而一旦作用域内出现赋值,读取指令就会被全部替换为局部变量读取指令 LOAD_FAST。 - [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 变量的作用域类型由 Python 在编译阶段通盘决定,并非按运行时代码执行的先后顺序动态判定。 - [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 当函数内未显式声明 global 且存在对变量的赋值操作时,Python 会默认将该变量视为当前函数的局部变量。 - [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 在函数中若需要对顶层全局变量进行重新绑定赋值,必须在作用域开头使用 global 关键字进行显式声明。 - [03:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=180s) 闭包结构中同样存在作用域判定机制,在内层嵌套函数中对同名变量赋值默认只会创建内层的局部变量,无法直接修改外层函数的变量值。 - [03:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=180s) 在闭包中尝试修改外层函数的自由变量时不能使用 global,而必须使用 nonlocal 关键字显式声明。 - [03:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=180s) “全局变量可读不可写”的说法语义含糊,更精确的判断标准是该变量名是否在当前作用域内出现在等号左边被重新赋值绑定。 - [03:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=180s) 对全局字典或列表进行原地修改(如 d['key'] = value)在字节码层面属于先读取全局变量再执行下标操作,未重新绑定变量本身,因此不需要 global 声明。
## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 全局变量“可读不可写”的报错困惑与字节码底层机制
在 Python 中使用全局变量时,很多初学者都会遇到一个极其违背直觉的陷阱。码农高天首先给出一个简单示例:外部定义了一个全局变量 `count = 0`,接着定义函数 `f()` 并在其中执行 `print(count)`,调用 `f()` 时程序会顺利打印出 `0`。然而,如果稍作改动,在函数内加入一行 `count = count + 1`,程序就会直接崩溃报错,提示局部变量在赋值前就被引用(UnboundLocalError)。许多开发者在懵圈之余会尝试调试,想看看函数内部的 `count` 到底是什么,于是在进入函数的第一行就先写 `print(count)`,试图在执行 `count = count + 1` 之前把值打出来;但令人惊讶的是,原本能正常打印的代码此时也会抛出同样的错误。
去网上搜索或求助他人后,通常得到的解决方案是在函数最开头加上一行 `global count` 显式声明,程序便会按照预期顺利运行。但背后的核心疑问依然存在:为什么原本好好的读取操作,一旦后续加上了赋值,前面就读不出来了?通过对比 Python 生成的字节码,码农高天揭示了底层真相: - 在最初只有读取的函数中,Python 加载变量时使用的是指令 `LOAD_GLOBAL`,即从全局作用域中查找变量。 - 当函数中加入了 `count = count + 1` 之后,加载指令瞬间变成了 `LOAD_FAST`,也就是读取局部变量。既然读取的是局部变量,而此时局部作用域内尚未对它完成赋值,自然就会直接抛出未定义异常。 - 当显式加上 `global count` 后,指令才重新被指定为 `LOAD_GLOBAL`。
码农高天强调,这是由于 Python 在编译阶段就决定了变量的作用域类型:“因为他在编译的时候,发现了一个对 count 的 assignment。当你在一个函数里尝试对一个变量赋值的时候,Python 就会默认这个变量是一个局部变量,无论你赋值的位置是在哪。”这意味着即使在第四行打印、第五行才赋值,Python 在编译期就已经把整个函数内的 `count` 标记成了局部变量,并在局部变量表中查找它,最终导致第四行的打印崩溃。码农高天总结道:“所以说当你的函数里存在对全局变量的赋值的时候,你就必须要显式的声明这个变量是一个全局变量;反之当你这个函数里只使用了这个变量而没有对这个变量赋值的时候,Python 就会机智的在全局变量里去寻找这个变量。”
```mermaid flowchart TD accTitle: Python函数内变量作用域的编译期判断逻辑 accDescr: 展示Python编译器根据函数内是否有赋值语句来决定使用LOAD_FAST还是LOAD_GLOBAL的过程 subgraph CompileStage["编译期分析 (Compile Time)"] A["检查函数内部作用域"] --> B{"是否存在对变量的赋值操作?"} B -->|"是 (且未显式声明)"| C["默认标记为局部变量 (Local)"] B -->|"否 (且非参数)"| D["标记为全局/自由变量 (Global/Free)"] C --> E["使用 LOAD_FAST 指令读取"] D --> F["使用 LOAD_GLOBAL 指令读取"] end subgraph RuntimeStage["运行期行为 (Runtime)"] E --> G{"读取时局部变量是否已赋值?"} G -->|"未赋值"| H["抛出 UnboundLocalError 报错"] G -->|"已赋值"| I["正常读取局部变量"] F --> J["在全局作用域中成功找到并读取"] end ```
[03:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=180s) 闭包自由变量、`nonlocal` 声明与“写”的本质判定
这种编译期作用域判定的机制不仅适用于全局变量,同样适用于闭包(Closure)结构中的自由变量。例如在外部函数 `G` 中定义 `count = 0`,内部嵌套函数 `F` 中执行赋值操作: 1. 如果在 `F` 中直接写 `count = count + 1`,同样会发生读取在赋值之前的未绑定错误。 2. 如果在 `F` 中直接写 `count = 1`,虽然不会报错,但在执行完 `F()` 之后,外部函数 `G` 内部的 `count` 依然是 `0`。这是因为 `count = 1` 在编译时被视为了 `F` 自身的局部变量,并没有通过闭包机制去修改外层 `G` 的变量。 3. 如果此时在 `F` 中误写为 `global count`,程序依然会出错,因为目标变量并非最外层的全局变量,而是属于闭包外层的自由变量。此时必须使用 `nonlocal` 关键字来显式声明该变量不是局部变量。
码农高天坦言,Python 的这个特性“说实话有一点点反人类,不是非常的直观”。坊间常有一种总结叫做“全局变量可读不可写”,虽然好记,但极易造成混淆。以字典为例,如果在外部定义一个全局变量字典 `d`,在函数 `F` 内部执行字典内容的修改(例如通过下标 `d['key'] = value` 赋值),代码完全可以正常运行。如果单从语义上讲这算不算对 `d` 的写入,定义是相当含糊的。但从底层的字节码视角看,该操作本质上是先通过 `LOAD_GLOBAL` 读取了全局字典 `d`,随后通过下标操作符(subscript)修改其内部内容,并没有对变量 `d` 本身进行重新绑定赋值。
码农高天指出,为了彻底避免概念混淆,不能单凭常识理解的“写入”,而必须严格看变量在当前作用域内是否被“赋值”: > “因此我觉得一个更不容易混淆的方法还是要去看这个变量在这个 scope 之内有没有被赋值,也就是他有没有出现在等号的前面。只要他被赋值了,他就会被默认成是一个局部变量;如果他没有被赋值,他又不是一个输入的 argument,Python 就会认为它是一个全局变量。”
最后,码农高天再次强调,赋值语句出现的物理顺序并不能改变这一规则,即便读取代码写在赋值代码的前面,Python 在编译时依然会通盘将其认作局部变量。
- **显式声明的作用场景区分**: - `global`:用于显式声明变量属于模块顶层的全局命名空间。 - `nonlocal`:用于闭包内部,显式声明变量绑定在外层嵌套函数的局部作用域(自由变量),而非全局作用域。 - **变量类型的判断核心依据**:是否作为赋值目标(出现在赋值号 `=` 的左侧,或被显式赋值语句直接绑定)。字典或列表的原地修改(如 `d[k] = v` 或 `lst.append()`)属于对已读取对象的内部修改,并不属于对变量符号本身的赋值绑定,因此不需要加 `global`。
## Notable quotes > [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 当你在一个函数里尝试对一个变量赋值的时候,Python 就会默认这个变量是一个局部变量,无论你赋值的位置是在哪。 — 码农高天
> [00:00](https://www.youtube.com/watch?v=tLcDQhy9Ew0&t=0s) 当你的函数里存在对全局变量的赋值的时候,你就必须要显式的声明这个变量是一个全局变量;反之当你这个函数里只使用了这个变量而没有对这个变量赋值的时候,Python 就会机智的在全局变量里去寻找这个变量。 — 码农高天
## Mentioned Python, 码农高天
## Video description 这期视频我们讲一下python中使用全局变量和自由变量时候的一些坑。到底什么时候需要加global和nonlocal声明呢?
[Source](https://www.youtube.com/watch?v=tLcDQhy9Ew0)