Python全局变量与局部变量的区别及作用域规则

Hazel
Hazel
发布时间:2026-11-09 | 9人浏览过
原创

深入解析Python中全局变量与局部变量的核心差异,系统阐述LEGB作用域查找机制,以及global与nonlocal关键字的实际应用和高频报错避坑指南。

Python进阶基础 作用域机制与语法规则 阅读时间:约10分钟

在 Python 编程中,变量的可见性、生命周期以及是否能在特定位置读取或修改,完全由变量所处的作用域决定。理解全局变量与局部变量的区别,掌握 Python 的 LEGB 作用域查找规则,是编写稳定代码、避免意外变量覆盖和彻底解决 UnboundLocalError 报错的基础。本文将直接切入两者的本质差异,详细拆解作用域查找流程与关键字控制,并提供经过验证的代码示例与排查方法。

Python全局变量与局部变量的区别及作用域规则
图 0:Python 变量作用域核心架构概览

一、全局变量与局部变量的核心差异

在 Python 中,判断一个变量是全局变量还是局部变量,根本依据在于它的定义位置以及绑定的命名空间(Namespace)。Python 并未采用类似 C/Java 的显式类型声明(如 int x),而是依据赋值发生的上下文环境决定归属。

全局变量(Global Variable)是在模块(文件)顶层直接声明的变量,也可以在类或函数外部定义。它的生命周期从被定义开始,贯穿整个程序的运行,直到模块卸载或进程结束。模块内的所有函数与类默认都可以直接读取全局变量。

局部变量(Local Variable)是在函数内部、方法体内或作为形参被定义的变量。它的作用域严格限定在当前函数体内,外部代码完全不可见。局部变量的生命周期与函数调用的执行帧绑定:每次调用函数时分配空间,调用执行完毕后立即被回收销毁。

对比维度 全局变量 (Global) 局部变量 (Local)
定义位置 .py 脚本或模块的最顶层代码块 函数内部、方法内部或函数形参列表
所属命名空间 模块命名空间(通过 globals() 查看) 局部栈帧命名空间(通过 locals() 查看)
作用域可见性 当前模块全局可见;其他模块导入后可见 仅在声明该变量的函数内部可见
生命周期 从执行声明开始,至程序退出时销毁 函数调用时创建,函数执行返回时销毁
内部修改机制 函数内若要重新赋值,须用 global 关键字 直接赋值即可,随函数结束而丢弃
Python全局变量与局部变量的区别及作用域规则
图 1:全局作用域与局部调用栈帧的命名空间隔离与访问关系

二、Python 作用域检索机制:LEGB 规则

当 Python 代码中出现一个变量名(如读取 print(count))时,解释器并不是全盘盲目搜索,而是遵循严格的由内向外(从具体到宽泛)单向查找机制。这套规则被称为 LEGB 规则,由四个英文单词首字母组成:

  1. L (Local) - 局部作用域:当前正在执行的函数体或 lambda 表达式内部。最先被检索。
  2. E (Enclosing) - 嵌套作用域(闭包外层):任何外层嵌套函数的作用域。当一个函数内定义了另一个函数时,内层函数能访问外层函数的局部变量。
  3. G (Global) - 全局作用域:当前执行模块(.py 文件)的顶层作用域。
  4. B (Built-in) - 内置作用域:Python 语言自带的内置模块命名空间,如 print、len、range、Exception 等内置函数与异常类。

查找行为的核心准则:解释器按照 L -> E -> G -> B 的次序依次检验对应命名空间。只要在某一层首先找到了该变量名,查找立即终止,直接取用该处的值;如果遍历到 Built-in 仍未命中,解释器便抛出标准的 NameError: name 'xxx' is not defined 异常。

Python全局变量与局部变量的区别及作用域规则
图 2:LEGB 单向顺序检索及最终判定流程

下面通过一段典型的嵌套函数代码,直接观察 LEGB 规则的级联效果:

# G: 全局作用域
x = "Global x"

def outer():
    # E: 闭包外层作用域
    x = "Enclosing x"
    
    def inner():
        # L: 局部作用域
        x = "Local x"
        print("1. inner 读取:", x)
        
    inner()
    print("2. outer 读取:", x)

outer()
print("3. 全局读取:", x)
示例运行输出
1. inner 读取: Local x
2. outer 读取: Enclosing x
3. 全局读取: Global x

当 inner() 内部声明了 x = "Local x" 时,根据 LEGB 顺序,查找在 Local 阶段即宣告命中,因此输出局部值。而当 inner() 执行结束后,其内部的 x 随着栈帧弹出而失效,outer() 读取到的是其自身的 Enclosing 级别变量;最外层的全局打印则取到 Global 级别的定义。各个命名空间互不侵扰。

三、变量的修改与跨作用域绑定:global 与 nonlocal

许多初学者会产生一个常见误解:“既然函数内可以读取全局变量,那我写 count = count + 1 就自然是在修改全局变量。”但实际运行却会直接抛出严重错误。这是因为 Python 对“变量读取(Reference)”与“变量赋值(Binding/Assignment)”的处理逻辑截然不同。

在 Python 中,当解释器在函数体内解析到一个赋值语句(包含 =、+=、-= 等)时,如果在函数内部未曾作任何特殊声明,Python 就会在编译生成字节码的阶段,直接将该变量硬编码标记为“局部变量(Local)”。

1. global 关键字:在函数内写入或声明全局变量

如果业务确实需要在函数内部对全局变量执行重新赋值(Re-binding),必须使用 global 关键字显式告知解释器:该变量属于模块顶层,不要在当前局部命名空间建立新变量。

# 全局变量
counter = 0

def increment():
    global counter  # 显式指示:此处的 counter 指向模块全局作用域
    counter += 1    # 正确修改全局变量

print("调用前:", counter)
increment()
increment()
print("调用后:", counter)
示例运行输出
调用前: 0
调用后: 2

2. nonlocal 关键字:在闭包中修改外层嵌套变量

在嵌套函数结构中,如果内层函数需要修改外层函数(但非全局模块)中的变量,global 关键字就无法适用了——因为 global 会直接跳过闭包层直奔模块全局。此时必须使用 Python 3 引入的 nonlocal 关键字。

def make_counter():
    count = 0  # 处于 outer 的局部 / inner 的 Enclosing
    
    def step():
        nonlocal count  # 显式指示:指向最近一层的外层函数局部变量
        count += 1
        return count
        
    return step

c = make_counter()
print(c())  # 输出: 1
print(c())  # 输出: 2
重要区别:可变对象原地修改(In-Place Mutation)无需 global

如果全局变量是一个可变对象(如 list、dict、自定义类的实例),在函数内部调用其变异方法(如 list.append()、dict.update() 或 dict['key'] = val),不需要使用 global 声明。因为这属于通过已有引用修改对象内部状态,并未改变变量名指向的内存引用地址。只有对变量名执行重新赋值绑定(如 my_list = [1, 2])时才需要 global。

Python全局变量与局部变量的区别及作用域规则
图 3:三种赋值场景下变量绑定行为与底层指针重定向示意

四、高频排查:UnboundLocalError 的底层成因与修复

在 Python 日常开发中,作用域引发的最典型错误莫过于:

UnboundLocalError: local variable 'val' referenced before assignment

许多开发者在面对如下代码时会感到困惑:

val = 10

def test():
    print("当前数值:", val)  # 此处直接报错!
    val = 20

test()

原因剖析:直觉上,读者可能认为执行到 print(val) 时,下一行的 val = 20 还没运行,因此应该先打印全局变量 10。然而,Python 并非逐行纯解释语言,而是在执行函数前先对其进行字节码编译。

在编译期,编译器扫描发现 test 函数体内存在赋值语句 val = 20,立刻将 val 归类为该函数的局部变量。在执行到第一行 print(val) 时,根据 LEGB 规则,局部作用域已然注册了该名字,于是查找直接在 Local 命中;但此时该局部变量尚未被执行赋值绑定(即该槽位尚未存储有效引用),因此直接抛出 UnboundLocalError。

Python全局变量与局部变量的区别及作用域规则
图 4:作用域异常定位与重构方案决策路径

五、最佳实践与编写建议

掌握作用域规则不仅有助于消除语法错误,更是写出高质量、可维护 Python 架构的关键。在日常编码中,建议遵循以下工程准则:

  • 严格控制全局变量数量:全局变量常驻内存,且任何地方都可以对其更改,极易导致模块间隐式耦合和难以复现的 Bug。如无特殊必要,尽量将全局状态包装为类属性、上下文对象或配置字典。
  • 遵循纯函数原则(Pure Functions):优先通过函数参数将数据传递进函数,并在处理完毕后通过 return 返回新结果,避免在函数体内依赖或篡改外部的全局状态。
  • 避免变量遮蔽(Shadowing Built-ins):绝不要将自定义变量命名为 list、dict、str、id、type 或 len。这样做虽不会直接报错,但会覆盖 Python 内置命名空间,导致后续需要调用对应内置功能时发生严重异常。
  • 善用调试工具:在排查复杂的动态命名空间时,可在运行期直接打印 locals() 与 globals() 的键值对,实时核对变量当前注册的归属层级。
相关专题:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表