一篇文章讲清楚Python的变量作用域

简介: 本文用一个调试2小时的`UnboundLocalError` Bug切入,生动讲解Python变量作用域核心概念——LEGB规则(局部→嵌套→全局→内置),通过快递站比喻、四层作用域详解、四大经典陷阱及实战代码,帮你彻底理解为何“能读不能改”,并掌握`global`/`nonlocal`正确用法。(239字)

免费编程软件「python+pycharm」

链接:https://pan.quark.cn/s/48a86be2fdc0

一个让我调试了2小时的bug

去年有个朋友发给我一段代码,让我帮忙看看问题出在哪。

他写了一个小程序,用来统计用户输入的数字:

total = 0

def process_numbers():
   while True:
       num = input("请输入数字(输入q退出):")
       if num == 'q':
           break
       total = total + int(num)
       print(f"当前总和:{total}")

process_numbers()
print(f"最终总和:{total}")

运行结果:

请输入数字(输入q退出):5
UnboundLocalError: local variable 'total' referenced before assignment

他懵了。total明明在函数外面定义好了,为什么报错说没定义?

他试了另一个写法:

count = 10

def show():
   print(count)

show()  # 输出10,正常运行

奇怪,这个就能读到。为什么一个能读,一个不能改?

那天晚上,我花了一个小时给他讲清楚什么是变量作用域。今天我把这些内容整理出来,希望帮你绕开同样的坑。

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


什么是作用域?一个快递站的比喻

先别急着看代码,我们来想一个生活中的场景。

你家住在一个小区。小区里有三个地方可以收快递:

  • 你家门口:只有你能拿,邻居拿不到
  • 小区快递柜:整个小区的业主都能用
  • 城市物流中心:全市都能用

变量作用域也是类似的概念。Python里的变量也分“放在哪”:

  • 局部变量:只属于当前函数,外面访问不到
  • 嵌套作用域:外层函数里的变量,内层函数可以访问
  • 全局变量:整个文件都能访问
  • 内置作用域:Python自带的,任何时候都能用

这就是著名的LEGB规则:Local → Enclosing → Global → Built-in。

别被这四个英文词吓到。用一个例子就能说清楚。


第一层:局部作用域(Local)

先看这段代码:

def greet():
   message = "你好"
   print(message)

greet()  # 输出:你好
print(message)  # 报错!NameError: name 'message' is not defined

message是在greet函数内部创建的,它只属于这个函数。函数执行完后,message就消失了。外面的代码看不到它。

这就是局部变量:函数内部定义的变量,外面访问不到。

反过来也一样:

name = "张三"

def say():
   age = 18
   print(name)  # 能读到外面的name
   print(age)   # 能读到自己的age

say()
print(age)  # 报错!外面读不到函数里的age

简单总结:函数里面能看到外面,外面看不到里面

就像一个房间:里面的人能看到窗外的街景,但街上的人看不到房间里放了什么。


第二层:全局作用域(Global)

全局变量定义在函数外面,整个文件里的代码都能访问它。

# 全局变量
app_name = "我的应用"
version = "1.0"

def show_info():
   print(app_name)  # 函数里可以读全局变量
   print(version)

show_info()  # 正常运行
print(app_name)  # 外面也能用

看起来很简单对吧?但坑马上就来了。

函数里可以读全局变量,但不能直接改

counter = 0

def increment():
   counter = counter + 1  # 报错!

increment()

为什么读可以,改不行?

因为当Python看到函数里有counter = xxx这种赋值语句时,它的第一反应是:“哦,你要在函数里创建一个新的局部变量,名字叫counter。”

然后它看到等号右边也有counter,就去局部作用域找这个新变量——还没创建完呢,怎么找得到?于是报错。

解决方法:明确告诉Python,“我要用的是外面的那个counter”。

counter = 0

def increment():
   global counter  # 声明:这个counter是全局的
   counter = counter + 1

increment()
print(counter)  # 1

global就像一把钥匙,让你走进全局变量的房间去修改它。


第三层:嵌套作用域(Enclosing)

函数里面还可以定义函数。这种嵌套函数里,就能访问外层函数的变量。

def outer():
   x = 10  # 外层函数的变量
   
   def inner():
       print(x)  # 内层函数可以读外层变量
   
   inner()

outer()  # 输出10

这就叫嵌套作用域(也叫闭包作用域)。inner函数可以访问outer函数里的x

但如果inner想修改x呢?

def outer():
   x = 10
   
   def inner():
       x = 20  # 这是在inner里创建了一个新的局部变量x
       print("inner里的x:", x)
   
   inner()
   print("outer里的x:", x)

outer()
# 输出:
# inner里的x: 20
# outer里的x: 10

和外层函数修改全局变量一样,默认情况下,内层函数对变量的赋值只会创建局部变量,不会影响外层函数的变量。

要修改外层函数的变量,需要用nonlocal

def outer():
   x = 10
   
   def inner():
       nonlocal x  # 声明:这个x来自外层函数
       x = 20
   
   inner()
   print(x)  # 20

outer()

nonlocalglobal很像,但nonlocal找的是外层函数的变量,不是全局变量。


第四层:内置作用域(Built-in)

这是Python自带的作用域,任何时候都能用。

print(len("hello"))  # len是内置函数
print(sum([1,2,3]))  # sum是内置函数
print(str(100))      # str是内置类型

你不需要导入任何东西就能用它们。

但有一个需要注意的地方:不要用内置函数的名字做变量名

len = 10  # 你定义了一个叫len的变量
print(len("hello"))  # 报错!因为len现在是10,不是函数了

这就叫“覆盖了内置作用域”。修复也很简单:换个变量名,或者删掉这个变量。


一张图看懂LEGB查找顺序

当你在代码里写一个变量名时,Python按这个顺序去找:

  1. Local:当前函数内部
  2. Enclosing:外层函数(如果有嵌套)
  3. Global:模块全局
  4. Built-in:Python内置

找到第一个匹配的就停下,找不到就报NameError

看个综合例子:

x = "global x"  # 全局

def outer():
   x = "outer x"  # 外层函数
   
   def inner():
       x = "inner x"  # 局部
       print(x)
   
   inner()

outer()  # 输出 "inner x"

如果注释掉inner里的x = "inner x"

def outer():
   x = "outer x"
   
   def inner():
       print(x)  # 没有局部x,就去外层找
   
   inner()

outer()  # 输出 "outer x"

再把outer里的x注释掉:

x = "global x"

def outer():
   def inner():
       print(x)  # 局部没有,外层没有,就去全局找
   
   inner()

outer()  # 输出 "global x"

最后,如果你定义了一个叫print的变量,Python也不会去找内置的print了:

print = "hello"
print("world")  # 报错,因为print现在是字符串,不能当函数调用


为什么要有作用域?直接全部用全局不好吗?

你可能觉得:作用域这么多层,好麻烦,为什么不把所有变量都设为全局?

答案很简单:混乱

想象一个5000行的文件,100个函数共享100个全局变量。你在修改一个函数时,根本不知道这个变量还在哪里被改过。改一处,崩十处。

作用域就是隔离区。每个函数有自己的小房间,房间里的变量只属于自己。这样你可以放心地写itempx这些名字,不用担心和别的函数冲突。

作用域也是一种文档。看到globalnonlocal,你就知道:“这个变量是共享的,改动它可能会影响其他地方。”


四个最容易踩的坑

坑1:以为if/for能创造作用域

if True:
   x = 10

print(x)  # 10,x还在!

for i in range(5):
   pass

print(i)  # 4,i还在!

很多语言里,iffor里的变量只在代码块内有效。Python不是。Python只有函数能创造新作用域,ifforwhilewith都不能。

坑2:在函数内意外创建局部变量

name = "张三"

def change():
   name = "李四"  # 这不是修改全局,是创建局部
   print("函数内:", name)

change()  # 函数内: 李四
print("全局:", name)  # 全局: 张三 —— 没变!

想改全局变量但忘记写global,Python不会报错,而是默默创建一个局部变量。这种“静默失败”比报错更难找。

坑3:在列表推导式里用变量

x = 10
squares = [x**2 for x in range(5)]
print(x)  # Python 3里还是10,Python 2里会变成4

Python 3修复了这个问题:列表推导式的变量不会泄漏到外部。但如果你用的是Python 2,就要小心。

坑4:在嵌套函数里用循环变量

funcs = []
for i in range(3):
   funcs.append(lambda: i)

for f in funcs:
   print(f())  # 2 2 2,不是0 1 2

所有lambda函数都记住了同一个i的最终值(2)。要解决这个问题,可以把i作为默认参数传进去:

funcs = []
for i in range(3):
   funcs.append(lambda i=i: i)  # 把当前i的值固定下来

for f in funcs:
   print(f())  # 0 1 2


实战:三个函数让你彻底明白

写三个简单的函数,涵盖了LEGB的所有情况。

函数1:只读

config = "production"

def get_env():
   return config  # 读全局,不需要global

print(get_env())  # production

函数2:修改

mode = "read"

def switch_to_write():
   global mode
   mode = "write"

switch_to_write()
print(mode)  # write

函数3:嵌套

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

最后一个例子展示了一个重要模式:闭包increment函数“记住”了count变量,即使make_counter已经执行完了,count也没有消失。


快速判断一个变量的作用域

给你一个简单的判断方法:

  1. 这个变量在函数内有没有被赋值?
  • 没有 → 去外层/全局找
  • 有 → 它是局部变量(除非你用globalnonlocal声明了)
  1. 如果它是局部变量,赋值之前能不能用到它?
  • 不能 → 会报UnboundLocalError

记住这两条,90%的作用域问题都能解决。


一张表总结

场景 写法 关键字 查找范围
函数内读变量 直接写 局部→嵌套→全局→内置
函数内修改全局变量 global x; x = 10 global 全局
嵌套函数修改外层变量 nonlocal x; x = 10 nonlocal 外层函数
读取内置函数 直接用 内置命名空间

回到开头的那个bug。我朋友的问题出在哪?

total = 0

def process_numbers():
   # 这里少了一行
   while True:
       num = input("请输入数字:")
       if num == 'q':
           break
       total = total + int(num)  # total被赋值,被视为局部变量

total = total + 1这个语句让Python把total当作局部变量。但局部变量total在赋值前就被用到了,所以报错。

修复方法就是在函数开头加上:

def process_numbers():
   global total
   # 剩下的代码不变

加上这一行之后,total就不再是局部变量了,而是指向全局的那个total

我朋友后来把代码改好了,还学会了用globalnonlocal。从那以后,他再也没被作用域的问题困住过。

希望看完这篇文章的你,也不会。

目录
相关文章
|
29天前
|
程序员 Python
global和nonlocal到底有什么区别?
本文以一次代码审查“社死”经历切入,深入剖析Python中`global`与`nonlocal`的关键区别:前者用于修改模块级全局变量,后者专用于嵌套函数中修改外层函数的局部变量。通过对比示例、典型场景、易错点及速查口诀,帮你彻底理清用法,告别混淆。
180 0
|
1月前
|
存储 人工智能 NoSQL
知识库构建:将采集到的数据存入向量数据库,打造企业私域知识库
本文手把手教你用OpenClaw构建企业AI知识库:解决PDF难检索、AI不懂业务、数据用完即弃等痛点。详解Builtin(开箱即用)、LanceDB(永久记忆)和阿里云Tablestore/Hologres(团队协作)三套向量数据库方案,含配置模板、命令及避坑指南,全程本地化、数据私有。(239字)
157 0
|
1月前
|
人工智能 运维 JavaScript
从零搭建 Hermes Agent 阿里云一键部署实操步骤及常见问题排查方案
Hermes Agent是一款面向多轮对话、长上下文交互与复杂任务拆解的开源AI智能体,凭借稳定的会话记忆能力、灵活的任务执行逻辑以及对主流大模型的良好适配性,被广泛应用于个人辅助办公、学习答疑、逻辑分析、小型团队协作等场景。相较于传统对话工具,该智能体可以完整保留多轮交互内容,对长链路指令、分步式任务有着更强的处理能力,也是当下技术爱好者与普通用户常用的开源项目之一。
264 0
|
4月前
|
XML 算法 数据格式
当Word文档里的图片成了“拦路虎”:用Python批量处理图片的实战指南
本文分享用Python批量处理Word文档图片的实战方案:利用python-docx提取图片、Pillow统一调整尺寸与格式,并自动插入新报告文档。附完整代码与常见问题(如浮动图、.doc格式)解决方案,助你3分钟完成原本一整天的手工活。
565 1
|
4月前
|
机器学习/深度学习 并行计算 算法
基于YOLOv8的5种玻璃缺陷识别(破裂/打胶/起霜/污染/未加工)(中英文双版) | 附完整源码与效果演示
本文介绍了一种基于YOLOv8的玻璃状况识别系统,该系统能够自动识别玻璃的五种常见缺陷:玻璃破裂、玻璃打胶、玻璃起霜、玻璃污染和玻璃未加工。通过采用先进的深度学习算法,实现了对玻璃产品的高精度、高效率检测,为工业生产提供了可靠的质量保障手段。
396 14
|
4月前
|
JSON 前端开发 API
用Pydantic实现Python数据校验的最佳实践
本文以小张调试用户注册报错为引,生动揭示Python后端数据校验混乱的痛点:规则散落、类型错误频发、业务逻辑被校验淹没。随即引出Pydantic解法——通过声明式模型(如`class User(BaseModel): username: str; age: int = Field(ge=18)`),实现自动类型转换、嵌套校验、字段约束与清晰错误提示,大幅提升代码可读性、健壮性与可维护性。(239字)
327 0
|
安全 大数据 测试技术
Mongodb亿级数据量的性能测试比较完整收藏一下
原文地址:http://www.cnblogs.com/lovecindywang/archive/2011/03/02/1969324.html 进行了一下Mongodb亿级数据量的性能测试,分别测试如下几个项目: (所有插入都是单线程进行,所有读取都是多线程进行) 1) 普通插入性能 (插...
4957 0
|
1月前
|
缓存 人工智能
Qwen3.7-Max评测——阿里云百炼智能体Agent模型,免费100万tokens快速体验
阿里云百炼发布Qwen3.7-Max智能体大模型,专为Agent场景优化。现享5折优惠:输入6元/百万tokens、输出18元/百万tokens,并免费赠送100万tokens快速体验!Arena盲测国产第一,性能领先Kimi、DeepSeek等。立即领取折扣券,上阿里云百炼平台快速体验:https://t.aliyun.com/U/fPVHqY
481 1
|
1月前
|
Java
【AgentScope Java新手村系列】(8)多Agent协作
多Agent协作 — orchestrator + workers 模式替代 MsgHub,一个 HarnessAgent 搭载多个 SubagentDeclaration,LLM 主持群聊与辩论。
342 0
|
3月前
|
资源调度 JavaScript 前端开发
前端组件库——Arco Design Vue知识点大全(一)
教程来源 https://bncne.cn/sheyingjiqiao.html Arco Design Vue是字节跳动开源的企业级Vue 3设计系统,源于抖音等亿级应用实践。聚焦设计一致性、开发提效与多端适配,具备60+高质量组件、全TypeScript支持、极致性能及可视化主题定制能力,已成为Vue 3生态中增长迅速的现代化UI解决方案。