global和nonlocal到底有什么区别?

一个让我在代码审查时社死的经历

去年公司做代码审查,我提交了一段这样的代码:

csharp 复制代码
counter = 0

def increment():
    counter += 1
    return counter

def outer():
    x = 10
    
    def inner():
        x += 1
        return x
    
    return inner()

审查官看了一眼,说:"你这两个函数都会报错,知道为什么吗?"

我当时脑子一热,脱口而出:"我知道,第一个要加global,第二个要加nonlocal。"

"那你加了没?"

"呃......忘了。"

审查官笑了笑:"你这不是忘了,你是分不清什么时候用global,什么时候用nonlocal。"

他说得对。我当时确实分不清。这两个关键字看起来差不多,都是在函数里声明"这个变量不是我本地的",但具体怎么用,脑子里是一团浆糊。

今天我就把这两个关键字的区别彻底讲清楚。看完之后,你绝对不会再搞混。


先看一个最简单的对比

python 复制代码
# global 的例子
x = 10  # 全局变量

def func1():
    global x
    x = 20  # 修改全局的x

func1()
print(x)  # 20


# nonlocal 的例子
def outer():
    y = 10  # 外层函数的变量
    
    def inner():
        nonlocal y
        y = 20  # 修改外层函数的y
    
    inner()
    print(y)  # 20

outer()

从代码上看,区别很明显:

  • global用在函数内部 ,目标是全局作用域的变量
  • nonlocal用在嵌套函数 内部,目标是外层函数的变量

但光看这个还不够。我们深入拆解一下。


global:走出函数,走到文件最外层

先看一个会报错的例子:

ini 复制代码
name = "张三"

def change():
    name = "李四"   # 这是创建了一个新的局部变量,不是修改全局的

change()
print(name)  # 输出"张三"------没变!

上面的代码不会报错,但是达不到你想要的效果。因为name = "李四"在函数内部创建了一个同名的局部变量,外面的name根本没动过。

如果加上global

ini 复制代码
name = "张三"

def change():
    global name
    name = "李四"   # 现在修改的是全局变量

change()
print(name)  # 输出"李四"

global的作用就是告诉Python:"别在函数里创建新变量,直接用外面的那个。"

global的核心规则:

  1. 只能在函数内部使用(模块顶层不需要,因为顶层本来就是全局作用域)
  2. 声明的变量名必须是全局作用域里已经存在的,或者你准备在全局创建它
  3. 一个函数里可以有多个global声明:global a, b, c

global的典型应用场景

场景1:修改配置变量

ini 复制代码
DEBUG = False

def enable_debug():
    global DEBUG
    DEBUG = True

def disable_debug():
    global DEBUG
    DEBUG = False

场景2:计数器

python 复制代码
call_count = 0

def track_call():
    global call_count
    call_count += 1
    print(f"这个函数被调用了{call_count}次")

场景3:在函数内部创建全局变量

csharp 复制代码
def create_global():
    global new_var
    new_var = "我是在函数里创建的全局变量"

create_global()
print(new_var)  # 正常输出

这种情况比较少见,但语法上是允许的。

global 容易踩的坑

坑1:在global声明之前使用变量

ini 复制代码
x = 10

def bad():
    print(x)   # 这里想读全局x
    global x   # 但是global声明应该在前面
    x = 20

bad()

这会报语法错误:SyntaxError: name 'x' is used prior to global declaration

正确写法:global声明要放在函数的最前面(在用到这个变量之前)。

坑2:在for/if里用global

go 复制代码
x = 10

def func():
    for i in range(3):
        global x  # 语法上可以,但没必要放在循环里
        x = i

func()
print(x)  # 2

global声明应该放在函数顶部,不要写在循环或条件语句里。虽然语法允许,但会让人困惑。

坑3:以为global可以跨文件

ini 复制代码
# file1.py
x = 10

# file2.py
from file1 import x

def change():
    global x  # 这个x并不是file1里的x!
    x = 20

global只在当前模块(当前文件)的全局作用域里生效。如果你从别的模块导入了一个变量,修改它不会影响原模块。

跨文件共享状态应该用模块对象(import file1; file1.x = 20),而不是global


nonlocal:走出内层函数,但只走到外层函数

nonlocal是Python 3才引入的。在Python 2里,嵌套函数想修改外层函数的变量,只能把变量做成列表或字典来绕过限制。

看一个会报错的例子:

csharp 复制代码
def outer():
    count = 0
    
    def inner():
        count += 1   # 报错!count被视为inner的局部变量
        return count
    
    return inner()

加上nonlocal

python 复制代码
def outer():
    count = 0
    
    def inner():
        nonlocal count   # 声明:这个count来自外层函数
        count += 1
        return count
    
    return inner()

print(outer())  # 1

nonlocal的核心规则:

  1. 只能在嵌套函数(函数里面定义的函数)内部使用
  2. 声明的变量必须在外层函数的局部作用域里已经存在
  3. 不能用来声明全局变量(会报语法错误)
  4. 不能跳过外层直接修改更外层的变量,它只找最近的一层

nonlocal的典型应用场景

场景1:闭包里的计数器

python 复制代码
def make_counter():
    count = 0
    
    def increment():
        nonlocal count
        count += 1
        return count
    
    return increment

counter = make_counter()
print(counter())  # 1
print(counter())  # 2
print(counter())  # 3

这是闭包的标准写法:外层函数创建一个变量,内层函数通过nonlocal修改它。

场景2:装饰器里维护状态

python 复制代码
def count_calls(func):
    call_count = 0
    
    def wrapper(*args, **kwargs):
        nonlocal call_count
        call_count += 1
        print(f"{func.__name__}被调用了{call_count}次")
        return func(*args, **kwargs)
    
    return wrapper

@count_calls
def say_hello():
    print("你好")

say_hello()  # say_hello被调用了1次
say_hello()  # say_hello被调用了2次

场景3:多层嵌套

python 复制代码
def outer():
    x = "outer"
    
    def middle():
        x = "middle"  # 这是middle的局部变量
        
        def inner():
            nonlocal x  # 这个x指向谁?
            x = "inner"
        
        inner()
        print(x)  # 输出"inner"
    
    middle()
    print(x)  # 输出"outer"------外层的x没有被影响

outer()

nonlocal查找规则:从当前函数(inner)的直接外层 开始找,找到的第一个同名变量就是目标。这里找到的是middle里的x,不是outer里的。

如果middle里没有定义xnonlocal会继续往outer里找。如果都找不到,报语法错误。


一张对比表,一目了然

特性 global nonlocal
用在哪里 任何函数内部 仅限嵌套函数内部
目标作用域 全局作用域 外层函数的局部作用域
目标变量必须存在? 否(可以在函数里创建新的全局变量) 是(必须已经在外层函数里定义)
能用在模块顶层吗? 不能(顶层不需要) 不能(顶层没有外层函数)
能跨文件吗? 不能(只在当前模块有效) 不适用
Python版本 所有版本 Python 3+

三个容易混淆的细节

细节1:nonlocal不能声明不存在的变量

python 复制代码
def outer():
    def inner():
        nonlocal x  # SyntaxError: no binding for nonlocal 'x' found
        x = 10

nonlocal要求变量已经存在于外层作用域。这和global不同,global可以在函数里创建新的全局变量。

细节2:在同一个作用域里,global和nonlocal不能混用

python 复制代码
x = 10

def outer():
    x = 20
    
    def inner():
        global x   # 指向全局的x(值为10)
        nonlocal x  # SyntaxError! 不能同时声明

一个变量要么是全局的,要么是外层函数的,不能同时是两者。

细节3:nonlocal只能往上找一层吗?

很多人以为nonlocal只能找直接外层,其实不是。它会一直往上找,直到找到最近的匹配:

python 复制代码
def outer():
    x = "outer"
    
    def middle():
        # middle没有定义x
        
        def inner():
            nonlocal x  # 从middle开始找,找不到,继续往外找,找到outer里的x
            x = "inner changed"
        
        inner()
    
    middle()
    print(x)  # "inner changed"

outer()

nonlocal会沿着嵌套层次一层层往上找,直到找到目标变量。但如果到了全局还没找到,就会报错(nonlocal不会到全局去找)。


实战:用闭包造一个"带状态的函数"

这是一个结合了nonlocal的经典例子:

python 复制代码
def create_account(initial_balance=0):
    balance = initial_balance
    transactions = []
    
    def deposit(amount):
        nonlocal balance
        balance += amount
        transactions.append(f"存入{amount}")
        return balance
    
    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            raise ValueError("余额不足")
        balance -= amount
        transactions.append(f"取出{amount}")
        return balance
    
    def get_balance():
        return balance
    
    def get_transactions():
        return transactions.copy()
    
    return deposit, withdraw, get_balance, get_transactions

deposit, withdraw, get_balance, get_transactions = create_account(100)
deposit(50)      # 150
withdraw(30)     # 120
print(get_balance())      # 120
print(get_transactions()) # ['存入50', '取出30']

这里用nonlocaldepositwithdraw能修改外层函数的balance变量。

如果不加nonlocalbalance += amount会在deposit内部创建局部变量balance,外面的balance不会变。


为什么Python要分这两个关键字?

一个常见的问题是:"为什么不统一用outer或者scope之类的关键字,非要分globalnonlocal?"

原因有两个:

1. 语义不同

global是从当前作用域直接跳到模块顶层,是一种"跳跃式"的访问。 nonlocal是沿着嵌套关系逐层往上找,是一种"爬楼梯式"的访问。

这两种行为不一样,用不同的关键字能清晰地表达意图。

2. 安全性

nonlocal不能用于全局变量,这防止了你在嵌套函数里无意中修改了全局状态。如果你确实想改全局变量,必须明确使用global

这种设计强迫程序员显式地声明"我知道这个变量是共享的,我负责"。


快速判断该用哪个

如果你需要修改一个变量,问自己三个问题:

问题1:这个变量在哪里定义的?

  • 在函数外面定义的 → 用global
  • 在外层函数里定义的 → 用nonlocal
  • 在内层函数里定义的 → 不需要任何关键字(它就是局部变量)

问题2:我现在在哪?

  • 在一个普通函数里 → 只能选global(如果确实需要改全局变量)
  • 在一个嵌套函数里 → 可能用global也可能用nonlocal,取决于目标变量在哪

问题3:如果不确定,先不加关键字,看报错信息

  • 报错说local variable referenced before assignment → 需要用globalnonlocal
  • 报错说no binding for nonlocal → 说明变量不在外层函数里,试试global
  • 报错说no binding for global → 说明变量不在全局里,试试nonlocal

一个终极测试

猜猜下面这段代码的输出是什么?

python 复制代码
x = "global"

def outer():
    x = "outer"
    
    def middle():
        x = "middle"
        
        def inner():
            nonlocal x
            x = "inner"
            print(f"inner里: {x}")
        
        inner()
        print(f"middle里: {x}")
    
    middle()
    print(f"outer里: {x}")

outer()
print(f"全局: {x}")

答案:

sql 复制代码
inner里: inner
middle里: inner
outer里: outer
全局: global

原因:

  • inner里的nonlocal x找到了middle里的x(最近的外层),把它改成"inner"
  • outer里的x没有被影响,因为nonlocal只往上找到最近的那一层就停了
  • 全局的x完全不受影响

如果你能完全说清楚这个输出,说明你已经彻底掌握了globalnonlocal的区别。


最后一句总结

  • global:我要修改全局变量,跟当前函数之外的文件顶层对话
  • nonlocal:我要修改外层函数的变量,跟外层的函数对话

记住这个简单的场景选择 :如果变量在函数外面(文件顶层),用global。如果变量在外层函数里,用nonlocal

就这一句话,够用了。

相关推荐
二月龙2 小时前
从零开发 Shiny 交互式数据看板:本地运行到网页上线完整路径
后端
小强19882 小时前
词云 + 情感分析:爬取评论数据做舆情可视化实战
后端
小强19882 小时前
高颜值动态可视化:gganimate 制作时序动图与数据短视频
后端
鱼人2 小时前
Shiny 模块化开发:大型数据分析平台拆分与代码复用实战
后端
长大19882 小时前
R 语言空间地图实战:从城市热力图到地理分布图,一篇吃透
后端
二月龙2 小时前
Shiny 对接 Excel / 数据库:从文件上传到自动分析
后端
JavaGuide2 小时前
Token 暴降 59%!这个项目让 Claude Code / Codex 不再满仓库乱翻。
后端·ai编程
Oneslide3 小时前
Vmware WorkStation Pro 下载和使用指南
后端
神奇小汤圆3 小时前
SwiftClockCache:一个高性能并发缓存的设计与实现
后端