我们来把“作用域”(Scope)这个编程中的核心概念拆解得明明白白。
### 核心比喻:全局村庄与函数作坊
想象你的整个 Python 程序是一个**“全局村庄”**。
* **全局作用域 (Global Scope)**: 就是村庄的**公共广场**。在广场上放置的任何东西(全局变量),比如一个公告板 `message = "Hello Village!"`,是所有村民都能看见和读取的。这个广场在村庄建立(程序开始)时就存在,直到村庄废弃(程序结束)时才消失。
* **局部作用域 (Local Scope)**: 当你调用一个函数时,就好比你临时搭建了一个**私密的“函数作坊”**。
* 这个作坊是完全封闭的。你在里面创建的任何工具或材料(局部变量)都只属于这个作坊。
* 当你的工作完成(函数返回),这个作坊会被**立即拆除**,里面所有的东西都会随之消失。下次再建同一个作坊,里面也是空空如也,不会记得上次留下的东西。
基于这个比喻,我们来逐一分析每个规则。
-----
### 1 局部变量不能在全局作用域内使用
**核心规则:** 作坊里的私有工具,在广场上是看不见也用不了的。
**原文代码分析:**
```python
def spam():
# ❶ 'eggs' 在 spam 作坊内部被创建
eggs = 31337
# 我们现在在村庄广场上
spam() # 1. 搭建 spam 作坊,创建 eggs=31337,然后工作完成,拆除作坊。
print(eggs) # 2. 在广场上大喊 "eggs 在哪?",但它已经随着作坊的拆除而消失了。
```
**详细解释:**
1. 程序执行到 `spam()` 时,Python 搭建了一个临时的 `spam` 作坊。
2. 在作坊内部,创建了一个名为 `eggs` 的工具,并给它贴上标签 `31337`。
3. `spam` 函数执行完毕并返回,Python 立即将整个 `spam` 作坊连同里面的 `eggs` 工具一起彻底拆除销毁。
4. 程序回到村庄广场(全局作用域),执行 `print(eggs)`。Python 在广场上寻找名为 `eggs` 的东西,但一无所获。因此,它只能报错:`NameError: name 'eggs' is not defined`(“找不到叫'eggs'的东西”)。
-----
### 2 局部作用域不能使用其他局部作用域内的变量
**核心规则:** 你在自己的作坊里,无法使用隔壁另一个作坊里的私有工具。
**原文代码分析:**
```python
def spam():
# ❶ spam 作坊的私有变量
eggs = 99
# ❷ 从 spam 作坊内部,要求搭建一个 bacon 作坊
bacon()
# ❸ bacon 作坊拆除后,回到 spam 作坊,打印自己作坊里的 eggs
print(eggs)
def bacon():
ham = 101
# ❹ bacon 作坊自己的私有变量,和 spam 里的那个无关
eggs = 0
# ❺ 在村庄广场上,要求搭建 spam 作坊
spam()
```
**执行流程的“作坊”视角:**
1. **`spam()` 被调用**: 搭建 `spam` 作坊。在作坊内,创建工具 `eggs = 99`。
2. **`bacon()` 被调用**: `spam` 的工作暂停。在 `spam` 作坊旁边,又搭建了一个全新的、独立的 `bacon` 作坊。
3. **进入 `bacon` 作坊**: 在这个新环境里,创建了 `ham = 101` 和 `eggs = 0`。这个 `eggs` 是 `bacon` 作坊的私有财产,和 `spam` 作坊里那个 `eggs` 毫无关系,它们只是恰好同名。
4. **`bacon()` 返回**: `bacon` 的工作完成,它的作坊被**立即拆除**。里面的 `ham` 和 `eggs=0` 全部消失。
5. **回到 `spam` 作坊**: 程序回到 `spam` 作坊被暂停的地方,继续执行 `print(eggs)`。此时,它会寻找\*\*当前所处作坊(`spam` 作坊)\*\*里的 `eggs`,找到的是 `99`。
6. **输出**: 屏幕上打印出 `99`。
7. **`spam()` 返回**: `spam` 工作完成,`spam` 作坊也被拆除。程序结束。
-----
### 3 全局变量可以在局部作用域中读取
**核心规则:** 在作坊里工作时,你可以随时探出头去看广场上的公共公告板。
**原文代码分析:**
```python
def spam():
# 在 spam 作坊里,需要用到 eggs
# Python: “先在 spam 作坊里找找有没有叫 eggs 的工具... 没有。”
# Python: “那我去村庄广场上看看有没有叫 eggs 的公告板... 啊哈,找到了!”
print(eggs)
# 在村庄广场上,立起一个公告板
eggs = 42
spam() # 搭建 spam 作坊
print(eggs)
```
**详细解释:**
这就是 Python 的变量查找规则,称为 **LEGB 规则**(Local -\> Enclosing -\> Global -\> Built-in)的简化版:
1. 当 `spam()` 函数中的 `print(eggs)` 执行时,Python 首先在**当前局部作用域**(`spam` 作坊)里寻找 `eggs`。
2. 它没有找到。于是,它会去**全局作用域**(村庄广场)寻找。
3. 在全局作用域中,它找到了 `eggs = 42`。于是,它读取这个值并打印出来。
4. 所以,第一个输出是 `42`。
5. `spam()` 返回后,`print(eggs)` 在全局作用域执行,自然也打印出 `42`。
-----
### 4 名称相同的局部变量和全局变量
**核心规则:** 如果你在作坊里创建了一个和广场公告板同名的私有工具,那么在作坊里你会优先使用自己的私有工具,它会\*\*“遮蔽”(Shadow)\*\*掉广场上的那个。
**原文代码分析:**
```python
def spam():
# ❶ spam 作坊的私有 'eggs',它遮蔽了全局的 'eggs'
eggs = 'spam local'
print(eggs)
def bacon():
# ❷ bacon 作坊的私有 'eggs'
eggs = 'bacon local'
print(eggs) # 打印 bacon 作坊自己的 eggs
spam() # 搭建并进入 spam 作坊
print(eggs) # 从 spam 返回后,打印 bacon 作坊自己的 eggs
# ❸ 村庄广场上的公告板 'eggs'
eggs = 'global'
bacon() # 开始搭建 bacon 作坊
print(eggs) # bacon 作坊拆除后,打印广场上的 eggs
```
**输出剖析:**
1. `bacon local`: `eggs = 'global'` 被设置。`bacon()` 被调用,在 `bacon` 作坊内,`eggs = 'bacon local'` 被创建。`print(eggs)` 打印的是这个局部的 `'bacon local'`。
2. `spam local`: 在 `bacon` 内部,`spam()` 被调用。在 `spam` 作坊内,又创建了一个更局部的 `eggs = 'spam local'`。`print(eggs)` 打印的是这个 `'spam local'`。`spam` 返回,这个变量消失。
3. `bacon local`: 执行流回到 `bacon` 作坊。再次 `print(eggs)`,打印的依然是 `bacon` 作坊自己的 `'bacon local'`。`bacon` 返回,它的 `eggs` 变量消失。
4. `global`: 执行流回到村庄广场。`print(eggs)` 打印的是广场上的全局变量 `'global'`。
### 一个重要的补充:`global` 关键字
你提供的文本没有提到,但至关重要的一点是:**如果你想在函数作坊内部,修改村庄广场上的全局公告板,该怎么办?**
默认情况下,如果你在函数内对一个变量进行赋值 (`=`),Python 会无条件地认为你正在创建一个**新的局部变量**。
**错误的例子(无法修改全局变量):**
```python
counter = 0
def increment():
# Python 认为这是一个新的、局部的 counter,和全局的那个无关
counter = counter + 1
print(f"Inside function: {counter}")
increment()
print(f"Outside function: {counter}")
# 输出会报错!因为在 `counter = ...` 这行,Python 尝试读取右边的 `counter`
# 来计算,但此时它已经决定左边的 `counter` 是个新的局部变量,
# 所以右边的局部 `counter` 还没有值。
# UnboundLocalError: local variable 'counter' referenced before assignment
```
**正确的做法:使用 `global` 关键字**
`global` 关键字就像一个声明:“**我接下来要操作的这个变量,不是我作坊里的私有工具,而是村庄广场上的那个公共设施!**”
```python
counter = 0
def increment():
# 郑重声明:我操作的是全局的 counter
global counter
counter = counter + 1
print(f"Inside function: {counter}")
increment()
print(f"Outside function: {counter}")
```
**输出:**
```
Inside function: 1
Outside function: 1
```
这次,函数成功地修改了全局变量的值。
### 总结
* **隔离性**: 作用域的主要目的是**保护**。它将函数封装成独立的单元,防止它们意外地修改程序其他部分的状态,这使得代码更易于维护和调试。
* **查找顺序**: Python 会优先在最内层的局部作用域查找变量,如果找不到,再逐层向外(全局)寻找。
* **遮蔽**: 内部作用域的同名变量会“隐藏”外部作用域的变量。
* **修改全局**: 必须使用 `global` 关键字才能在函数内部修改全局变量。在大型程序中,应谨慎使用此功能。