去年有个朋友发给我一段代码,让我帮忙看看问题出在哪。
他写了一个小程序,用来统计用户输入的数字:
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,正常运行奇怪,这个就能读到。为什么一个能读,一个不能改?
那天晚上,我花了一个小时给他讲清楚什么是变量作用域。今天我把这些内容整理出来,希望帮你绕开同样的坑。
先别急着看代码,我们来想一个生活中的场景。
你家住在一个小区。小区里有三个地方可以收快递:
变量作用域也是类似的概念。Python里的变量也分“放在哪”:
这就是著名的LEGB规则:Local → Enclosing → Global → Built-in。
别被这四个英文词吓到。用一个例子就能说清楚。
先看这段代码:
def greet():
message = "你好"
print(message)
greet() # 输出:你好
print(message) # 报错!NameError: name 'message' is not definedmessage是在greet函数内部创建的,它只属于这个函数。函数执行完后,message就消失了。外面的代码看不到它。
这就是局部变量:函数内部定义的变量,外面访问不到。
反过来也一样:
name = "张三"
def say():
age = 18
print(name) # 能读到外面的name
print(age) # 能读到自己的age
say()
print(age) # 报错!外面读不到函数里的age简单总结:函数里面能看到外面,外面看不到里面。
就像一个房间:里面的人能看到窗外的街景,但街上的人看不到房间里放了什么。
全局变量定义在函数外面,整个文件里的代码都能访问它。
# 全局变量
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) # 1global就像一把钥匙,让你走进全局变量的房间去修改它。
函数里面还可以定义函数。这种嵌套函数里,就能访问外层函数的变量。
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()nonlocal和global很像,但nonlocal找的是外层函数的变量,不是全局变量。
这是Python自带的作用域,任何时候都能用。
print(len("hello")) # len是内置函数
print(sum([1,2,3])) # sum是内置函数
print(str(100)) # str是内置类型你不需要导入任何东西就能用它们。
但有一个需要注意的地方:不要用内置函数的名字做变量名。
len = 10 # 你定义了一个叫len的变量
print(len("hello")) # 报错!因为len现在是10,不是函数了这就叫“覆盖了内置作用域”。修复也很简单:换个变量名,或者删掉这个变量。
当你在代码里写一个变量名时,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个全局变量。你在修改一个函数时,根本不知道这个变量还在哪里被改过。改一处,崩十处。
作用域就是隔离区。每个函数有自己的小房间,房间里的变量只属于自己。这样你可以放心地写i、temp、x这些名字,不用担心和别的函数冲突。
作用域也是一种文档。看到global或nonlocal,你就知道:“这个变量是共享的,改动它可能会影响其他地方。”
坑1:以为if/for能创造作用域
if True:
x = 10
print(x) # 10,x还在!
for i in range(5):
pass
print(i) # 4,i还在!很多语言里,if或for里的变量只在代码块内有效。Python不是。Python只有函数能创造新作用域,if、for、while、with都不能。
坑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里会变成4Python 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也没有消失。
给你一个简单的判断方法:
global或nonlocal声明了)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。
我朋友后来把代码改好了,还学会了用global和nonlocal。从那以后,他再也没被作用域的问题困住过。
希望看完这篇文章的你,也不会。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。