global和nonlocal到底有什么区别?

简介: 本文以一次代码审查“社死”经历切入,深入剖析Python中`global`与`nonlocal`的关键区别:前者用于修改模块级全局变量,后者专用于嵌套函数中修改外层函数的局部变量。通过对比示例、典型场景、易错点及速查口诀,帮你彻底理清用法,告别混淆。

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

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

counter = 0

def increment():
   counter += 1
   return counter

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

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

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

“那你加了没?”

“呃……忘了。”

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

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

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

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (22).png


先看一个最简单的对比

# 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:走出函数,走到文件最外层

先看一个会报错的例子:

name = "张三"

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

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

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

如果加上global

name = "张三"

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

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

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

global的核心规则:

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

global的典型应用场景

场景1:修改配置变量

DEBUG = False

def enable_debug():
   global DEBUG
   DEBUG = True

def disable_debug():
   global DEBUG
   DEBUG = False

场景2:计数器

call_count = 0

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

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

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

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

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

global 容易踩的坑

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

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

x = 10

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

func()
print(x)  # 2

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

坑3:以为global可以跨文件

# 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里,嵌套函数想修改外层函数的变量,只能把变量做成列表或字典来绕过限制。

看一个会报错的例子:

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

加上nonlocal

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:闭包里的计数器

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:装饰器里维护状态

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:多层嵌套

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不能声明不存在的变量

def outer():
   def inner():
       nonlocal x  # SyntaxError: no binding for nonlocal 'x' found
       x = 10

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

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

x = 10

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

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

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

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

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的经典例子:

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

一个终极测试

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

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}")

答案:

inner里: inner
middle里: inner
outer里: outer
全局: global

原因:

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

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


最后一句总结

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

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

就这一句话,够用了。

目录
相关文章
|
26天前
|
小程序 Python
一篇文章讲清楚Python的变量作用域
本文用一个调试2小时的`UnboundLocalError` Bug切入,生动讲解Python变量作用域核心概念——LEGB规则(局部→嵌套→全局→内置),通过快递站比喻、四层作用域详解、四大经典陷阱及实战代码,帮你彻底理解为何“能读不能改”,并掌握`global`/`nonlocal`正确用法。(239字)
90 0
|
7月前
|
Web App开发 存储 人工智能
AI 英语学习智能体的开发
AI英语学习智能体已进化为具备感知、规划、记忆与执行能力的自主教学系统。本文涵盖核心架构、技术栈选型、开发模块与流程,指导从MVP到企业级落地,建议聚焦细分场景切入,如雅思口语或外贸陪练,实现高效低成本开发。(238字)
uni-app实现swiper滑动放大缩小、实现scroll-view与swiper双向联动
uni-app实现swiper滑动放大缩小、实现scroll-view与swiper双向联动
2152 0
|
9月前
|
缓存 运维 监控
vLLM推理加速指南:7个技巧让QPS提升30-60%
GPU资源有限,提升推理效率需多管齐下。本文分享vLLM实战调优七招:请求塑形、KV缓存复用、推测解码、量化、并行策略、准入控制与预热监控。结合代码与数据,助你最大化吞吐、降低延迟,实现高QPS稳定服务。
2483 7
vLLM推理加速指南:7个技巧让QPS提升30-60%
|
6月前
|
存储 缓存 固态存储
如何恢复格式化的电脑文件数据
误格式化≠数据消失!快速格式化仅清除索引,文件仍可恢复。关键:立即停止写入、勿装软件、避免覆盖。用专业工具扫描并预览,找回文件后另存他盘。注意SSD的TRIM与文件碎片化影响。冷静+正确操作=高概率拯救数据。备份才是根本!(239字)
|
7月前
|
安全 小程序 JavaScript
OAuth2.0四种授权模式
OAuth2四种授权模式简介:授权码模式最安全,适用于第三方登录;简化模式适用于无后端的应用;密码模式需高度信任;客户端模式用于服务间调用,无需用户参与。
|
物联网 程序员 芯片
你知道Hi3861芯片吗,支持OpenHarmony系统
本文介绍华为Hi3861芯片,该芯片集成了WiFi和蓝牙功能,具备低功耗、高集成度、强大通信能力和丰富接口资源,广泛应用于智能家居、智能穿戴、工业物联网和智慧城市等领域,助力开发者实现高效物联网解决方案。
918 0
你知道Hi3861芯片吗,支持OpenHarmony系统
|
数据采集 存储 JSON
从零到一构建网络爬虫帝国:HTTP协议+Python requests库深度解析
在网络数据的海洋中,网络爬虫遵循HTTP协议,穿梭于互联网各处,收集宝贵信息。本文将从零开始,使用Python的requests库,深入解析HTTP协议,助你构建自己的网络爬虫帝国。首先介绍HTTP协议基础,包括请求与响应结构;然后详细介绍requests库的安装与使用,演示如何发送GET和POST请求并处理响应;最后概述爬虫构建流程及挑战,帮助你逐步掌握核心技术,畅游数据海洋。
467 3
查发票|发票查验|验证码识别|批量查验|发票验真
昨天我开始对发票查验最耗时的环节-验证码识别下手,通过使用5000多张打码图片对PaddleOCR的模型进行训练后,识别率获得了很大提升。
852 1
|
机器学习/深度学习 人工智能 达摩院
DDColor:AI图像着色工具,优秀的黑白图像上色模型,支持双解码器!
DDColor:AI图像着色工具,优秀的黑白图像上色模型,支持双解码器!
1148 3