CC++ & Algorithm

Python变量作用域规则

困难4
语言版本:C++Python
概述:Python查找变量时遵循“由内向外、先局部再全局”的规则,就像找钥匙先从自己口袋找起。

变量作用域:Python 是怎么找到你的变量的?

你在写 Python 代码的时候,是不是经常遇到这种问题:在函数里面写了个变量,程序却报错说“没定义”?或者在函数内部改了某个变量,外面的值也跟着变了?搞清楚这些问题的关键,就是理解 变量作用域。简单说,作用域就是变量能够被访问到的“地盘”或“范围”。Python 有一套非常明确的规则来决定:当你写了一个变量名,程序会按什么顺序、去哪里找这个变量。这套规则叫做 LEGB 规则,我们今天就用学生生活中常见的例子把它彻底讲明白。

1. 找变量就像找自己的东西

想象一下你在学校的一天:上课时你突然发现铅笔不见了,你会怎么做?先翻自己的笔袋(自己的小范围),如果不在,就问问同桌(小组成员),还没有就看看教室的公共笔筒(班级),再不行就去广播室找(全校失物招领处)。Python 找变量的顺序也差不多:从最小的范围开始,一层层往外找,直到找到为止,如果所有地方都找不到,就报错。

这个顺序就是 LEGB,四个字母分别代表四种作用域:

  • L (Local) – 局部作用域,当前函数内部
  • E (Enclosing) – 外层函数的作用域(用于嵌套函数)
  • G (Global) – 全局作用域,模块最顶层
  • B (Built-in) – 内置作用域,Python 自带的函数和常量(比如 printlenTrue

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 自带的函数和常量,比如 printlenintTrueFalse,不需要你定义,在哪里都能直接用。这就是内置作用域。它相当于是“公共知识”,连刚入学的小朋友都知道什么是“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

如果不写 nonlocalcount = 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 导入时要注意名字冲突。
  • globalnonlocal 的最佳实践:尽量少用全局变量,多用参数和返回值,代码更容易理解和调试。

如果你觉得作用域有点晕,别怕——多写几个小函数试试,用 print 把中间结果打出来,就能慢慢摸清 Python 的“找钥匙”路线了。

例题精讲

1单选题

在Python中,当在函数内部访问一个变量时,解释器按照什么顺序查找该变量?

A局部作用域 -> 闭包作用域 -> 全局作用域 -> 内置作用域
B全局作用域 -> 局部作用域 -> 闭包作用域 -> 内置作用域
C内置作用域 -> 全局作用域 -> 闭包作用域 -> 局部作用域
D局部作用域 -> 全局作用域 -> 闭包作用域 -> 内置作用域
2判断题

在函数内部,如果直接对一个全局变量赋值(如 x = 1),而不使用 global 声明,则会创建一个新的局部变量。

3填空题
在嵌套函数中,如果希望修改外层函数的局部变量,需要使用 ___ 关键字。
4单选题

执行以下代码后,输出是什么? x = 10 def func(): x = 20 func() print(x)

A10
B20
C报错
DNone
5填空题
请补全代码,使得函数能正确修改全局变量 count 的值。
count = 0
def add():
    ___ count += 1
add()
print(count)  # 输出 1