Python变量作用域规则
困难4变量作用域:Python 是怎么找到你的变量的?
你在写 Python 代码的时候,是不是经常遇到这种问题:在函数里面写了个变量,程序却报错说“没定义”?或者在函数内部改了某个变量,外面的值也跟着变了?搞清楚这些问题的关键,就是理解 变量作用域。简单说,作用域就是变量能够被访问到的“地盘”或“范围”。Python 有一套非常明确的规则来决定:当你写了一个变量名,程序会按什么顺序、去哪里找这个变量。这套规则叫做 LEGB 规则,我们今天就用学生生活中常见的例子把它彻底讲明白。
1. 找变量就像找自己的东西
想象一下你在学校的一天:上课时你突然发现铅笔不见了,你会怎么做?先翻自己的笔袋(自己的小范围),如果不在,就问问同桌(小组成员),还没有就看看教室的公共笔筒(班级),再不行就去广播室找(全校失物招领处)。Python 找变量的顺序也差不多:从最小的范围开始,一层层往外找,直到找到为止,如果所有地方都找不到,就报错。
这个顺序就是 LEGB,四个字母分别代表四种作用域:
- L (Local) – 局部作用域,当前函数内部
- E (Enclosing) – 外层函数的作用域(用于嵌套函数)
- G (Global) – 全局作用域,模块最顶层
- B (Built-in) – 内置作用域,Python 自带的函数和常量(比如
print、len、True)
Python 会 从最里面的 L 开始找,找不到就往外一层 E,再找不到就 G,最后 B。一旦找到,就用这个变量的值;如果 B 也找不到,就会抛出 NameError。
为什么需要这套规则?因为同一个变量名可以在不同作用域中出现,Python 必须知道该用哪一个。规则就是为了避免混乱,让你能控制变量的可见范围。
2. 四个作用域用生活例子讲清楚
2.1 局部作用域(Local):你自己的零花钱
每个函数都有自己的“私房钱”——局部变量,只在函数内部有效。比如:
def buy_snack():
money = 5 # money 是局部变量,只在 buy_snack 函数里能用
print("我有", money, "元,去买零食")
buy_snack() # 输出:我有 5 元,去买零食
# print(money) # 这行会报错,因为 money 在函数外面不存在
就像你口袋里的零花钱,走出教室门可能就不属于这个函数的“口袋”了。
2.2 外层函数作用域(Enclosing):求助隔壁班的朋友
有时候函数里还嵌套着函数(就像班级里有小组),内层函数可以访问外层函数的变量,但反过来不行。
def class_activity():
teacher = "王老师" # 外层函数的局部变量
def group_activity():
print("我们小组的活动由", teacher, "负责") # 内层可以访问外层 teacher
group_activity()
# print(group_teacher) # 外层不能访问内层的变量
class_activity()
这就像你找不到铅笔,先问同小组的组员,组员也没有再去问班主任。注意:内层函数只能“读取”外层变量,如果要修改,需要特殊声明(后面会讲)。
2.3 全局作用域(Global):全校的公共规章制度
写在任何函数外边的变量,就是全局变量,在程序的所有地方都能看到(除非被局部变量遮住了)。比如:
school_name = "阳光小学" # 全局变量,整个程序都能用
def say_school():
print("我来自", school_name) # 函数里能读到全局变量
say_school() # 输出:我来自 阳光小学
print(school_name) # 全局中也能直接访问
全局变量就像学校的校名、校规,全校师生都知道。
2.4 内置作用域(Built-in):每个人都知道的“常识”
Python 自带的函数和常量,比如 print、len、int、True、False,不需要你定义,在哪里都能直接用。这就是内置作用域。它相当于是“公共知识”,连刚入学的小朋友都知道什么是“print”。
print("你好") # print 是内置函数
print(len("abc")) # len 也是内置函数
3. LEGB 查找顺序:实际演练
把四个作用域串起来,看看 Python 是怎么按顺序找的。下面的例子用“活动”作比喻,和原文一样,但加上了注释解释每一步查找过程:
# 全局变量
activity = "全校活动" # 全局变量
def classroom():
activity = "教室活动" # 外层函数(Enclosing)的局部变量
def group():
activity = "小组活动" # 最内层(Local)的局部变量
print("group中:", activity) # 先从 Local 找,找到 "小组活动"
group()
print("classroom中:", activity) # group 函数结束后,这里只有 Enclosing,找到 "教室活动"
classroom()
print("全局中:", activity) # 函数外,从 Global 找,找到 "全校活动"
输出结果:
group中: 小组活动
classroom中: 教室活动
全局中: 全校活动
注意每个作用域都有自己的 activity,互不干扰。Python 每次查找都是从内向外找离自己最近的那个。
4. 使用变量 vs 创建变量:两个不同的操作
很多新手会搞混“使用变量(读取值)”和“创建变量(赋值)”。规则其实不一样:
- 使用变量(读值):按照 LEGB 从内往外找。
- 创建变量(赋值):默认在当前最小的作用域创建一个新变量,不会修改外层的同名变量(除非你用特殊关键字声明)。
看这个零花钱的例子:
pocket_money = 100 # 全局变量,妈妈给的零花钱总金额
def buy_game():
pocket_money = 50 # 这里创建了一个新的局部变量 pocket_money,不会修改全局的!
print("买游戏后,函数里认为口袋还剩", pocket_money, "元")
buy_game()
print("实际上全局的零花钱还是", pocket_money, "元") # 输出 100
输出:
买游戏后,函数里认为口袋还剩 50 元
实际上全局的零花钱还是 100 元
为什么?因为在函数内部 pocket_money = 50 这行,Python 把它当作创建一个新的局部变量,而不是修改全局变量。它就像你在自己口袋里重新放了一笔钱,不会影响妈妈钱包里的钱。
5. 如何修改外部的变量:global 和 nonlocal
如果你真的想在函数内部修改全局变量,或者修改外层函数的变量,就需要用到两个关键字:
global:告诉 Python “我要修改的是全局变量”。nonlocal:告诉 Python “我要修改的是外层函数(非全局)的变量”。
5.1 用 global 修改全局变量
score = 80 # 全局变量,考试成绩
def add_bonus():
global score # 声明 score 是全局变量
score = score + 10 # 这样就能修改全局的 score 了
print("加分后内部:", score)
add_bonus()
print("加分后外部:", score) # 输出 90
没有 global 的话,score = score + 10 会先读取全局的 score(右边),然后创建一个新的局部变量 score,根本改不了全局。加上 global 后,整个函数内对该名字的操作都针对全局变量。
5.2 用 nonlocal 修改外层函数的变量
嵌套函数中,如果要修改外层函数的局部变量,就用 nonlocal。
def outer():
count = 0 # 外层函数的局部变量
def inner():
nonlocal count # 声明 count 来自外层函数
count = count + 1
print("内层 count:", count)
inner()
print("外层 count:", count)
outer()
输出:
内层 count: 1
外层 count: 1
如果不写 nonlocal,count = count + 1 会在内层创建一个新的局部变量 count,外层的 count 不会被修改。
6. 新手最容易犯的错误
❌ 错误1:在函数内部意外遮蔽全局变量
name = "小明"
def greet():
name = name + "你好" # 报错!先用了 name(读取),但 name 还没在局部创建
这种情况会报 UnboundLocalError。因为 Python 看到左边有赋值,就认为 name 是局部变量,但在右边读取时,局部变量还没被赋值,所以出错。正确做法:要么用 global,要么换一个变量名。
❌ 错误2:忘记 global 导致修改不生效
total = 0
def add():
total = total + 1 # 本意是累加,但这里创建了局部变量,不会修改全局
add()
print(total) # 输出 0,不是 1
❌ 错误3:在嵌套函数中用 nonlocal 修改全局变量
x = 10
def outer():
nonlocal x # 错误!nonlocal 只能用于外层函数的变量,不能用于全局
x = 20
这时应该用 global,而不是 nonlocal。
❌ 错误4:认为函数内部能直接修改不可变全局对象
对于整数、字符串、元组这些不可变对象,不声明 global 不能修改。但对于列表、字典这样的可变对象,你可以修改它的内容(比如添加元素),而不需要 global,因为这是“修改对象本身”而不是“重新赋值”。
classmates = ["小红", "小刚"] # 全局列表
def add_student():
classmates.append("小丽") # 可以!这是修改列表内容,不是重新赋值
add_student()
print(classmates) # 输出 ['小红', '小刚', '小丽']
但如果你写 classmates = ["新列表"],那就是重新赋值,会创建局部变量,需要 global。
7. 完整示例:零花钱管理系统
下面是一个综合例子,用到上面所说的所有规则,包括 LEGB 查找、global 和 nonlocal。
# 全局变量:家庭总零花钱支出
total_spent = 0 # 全局支出总额
def family_account():
"""家庭账户函数,包含孩子的零花钱管理"""
child_pocket = 20 # 外层函数变量:孩子当前零花钱余额
def buy_book(price):
"""买书,以内层函数模拟日常消费"""
nonlocal child_pocket # 声明要修改外层函数的 child_pocket
global total_spent # 声明要修改全局变量 total_spent
if child_pocket >= price:
child_pocket = child_pocket - price # 从孩子口袋扣钱
total_spent = total_spent + price # 家庭总支出增加
print(f"买书花了{price}元,孩子还剩{child_pocket}元")
else:
print(f"钱不够!孩子只有{child_pocket}元,需要{price}元")
# 买两本书
buy_book(15)
buy_book(10)
print("孩子最终余额:", child_pocket) # 外层函数读取修改后的值
family_account()
print("家庭总支出:", total_spent) # 全局变量被修改
运行结果:
买书花了15元,孩子还剩5元
钱不够!孩子只有5元,需要10元
孩子最终余额: 5
家庭总支出: 15
这个例子中:
- 内层函数
buy_book中用了nonlocal child_pocket修改外层child_pocket。 - 用了
global total_spent修改全局变量。 - 变量查找顺序:内层自己没有
child_pocket,就到外层(Enclosing)找;没有total_spent,就到全局(Global)找。
8. 相关知识点指引
掌握了变量作用域,下一步可以学习:
- 函数的参数传递:参数也是局部变量,理解作用域能帮你搞懂参数和全局变量的关系。
- 闭包(Closure):嵌套函数加上
nonlocal,就是闭包的初步形态,常用在回调、装饰器中。 - 模块的命名空间:每个模块都有自己的全局作用域,用
import导入时要注意名字冲突。 global和nonlocal的最佳实践:尽量少用全局变量,多用参数和返回值,代码更容易理解和调试。
如果你觉得作用域有点晕,别怕——多写几个小函数试试,用 print 把中间结果打出来,就能慢慢摸清 Python 的“找钥匙”路线了。
例题精讲
在Python中,当在函数内部访问一个变量时,解释器按照什么顺序查找该变量?
在函数内部,如果直接对一个全局变量赋值(如 x = 1),而不使用 global 声明,则会创建一个新的局部变量。
在嵌套函数中,如果希望修改外层函数的局部变量,需要使用 ___ 关键字。执行以下代码后,输出是什么? x = 10 def func(): x = 20 func() print(x)
请补全代码,使得函数能正确修改全局变量 count 的值。
count = 0
def add():
___ count += 1
add()
print(count) # 输出 1