前几天我在写一个Python脚本时遇到了一个奇怪的错误:UnboundLocalError: local variable 'x' referenced before assignment。这个错误让我困惑了很久,明明在函数外部已经定义了变量x,为什么在函数内部使用时会报错?相信很多Python开发者都遇到过类似的问题。
让我们先来看一个最简单的例子:
python复制x = 10
def show_x():
print(x)
show_x() # 输出10,没有问题
这段代码运行得很好,函数内部可以正常访问外部定义的变量x。但是如果我们稍微修改一下:
python复制x = 10
def show_and_change():
print(x)
x = 20 # 添加这行赋值语句
show_and_change() # 报错!
突然就出现了UnboundLocalError错误。这看起来很不合理,因为x明明已经在函数外部定义好了。为什么添加一个赋值语句就会导致前面的print语句出错呢?
要理解这个现象,我们需要了解Python的变量作用域规则,也就是常说的LEGB规则:
当我们在函数内部访问一个变量时,Python会按照L→E→G→B的顺序查找这个变量。这就是为什么第一个例子中函数可以访问外部变量x。
但是当函数内部出现对变量的赋值语句时,情况就变了。Python会在编译函数时(注意是编译时,不是运行时)确定哪些变量是局部变量。任何在函数内被赋值的变量(除非用global或nonlocal声明)都会被当作局部变量。
这就是为什么第二个例子会报错:虽然print(x)在x=20之前,但Python在编译函数时已经确定x是局部变量,而print(x)执行时局部变量x还没有被赋值。
Python解释器在编译函数代码时,会分析函数体中的所有变量引用和赋值操作,并确定每个变量的作用域。这个过程发生在函数定义时,而不是函数调用时。
让我们看一个更复杂的例子:
python复制x = 10
def confusing():
print(x) # 这里x是全局变量还是局部变量?
x = x + 1 # 这行决定了x是局部变量
print(x)
confusing() # 报错!
在这个例子中,虽然第一眼看上去第一个print(x)应该访问全局变量x,但实际上Python在编译函数时看到后面有x=x+1,就把x标记为局部变量,导致第一个print(x)也尝试访问局部变量x。
我们可以用dis模块查看函数的字节码来理解这个过程:
python复制import dis
x = 10
def func():
print(x)
x = 20
dis.dis(func)
输出结果会显示Python如何加载变量x。你会发现在print(x)处,Python尝试使用LOAD_FAST(用于局部变量)而不是LOAD_GLOBAL(用于全局变量)。
解决这个问题最直接的方法就是使用global关键字:
python复制x = 10
def correct_func():
global x # 声明x是全局变量
print(x) # 现在可以正常访问全局x
x = 20 # 修改的是全局x
correct_func()
print(x) # 输出20,全局x已被修改
global关键字告诉Python解释器:"这个函数中出现的x应该引用全局变量x,而不是创建一个新的局部变量。"
虽然global可以解决问题,但过度使用全局变量通常被认为是不好的编程实践。以下是一些使用建议:
嵌套函数中访问外部函数变量时,可能会遇到类似的问题:
python复制def outer():
x = 10
def inner():
print(x)
x = 20 # 这会导致UnboundLocalError
inner()
outer()
解决方法是用nonlocal关键字:
python复制def outer():
x = 10
def inner():
nonlocal x # 声明x来自外层函数
print(x)
x = 20
inner()
print(x) # 输出20
outer()
在类方法中访问实例变量不会遇到这个问题,因为实例变量是通过self明确访问的:
python复制class MyClass:
def __init__(self):
self.x = 10
def method(self):
print(self.x) # 通过self访问
self.x = 20 # 修改实例变量
obj = MyClass()
obj.method()
有趣的是,对于可变对象如列表和字典,修改内容不会被视为赋值操作:
python复制my_list = [1, 2, 3]
def modify_list():
print(my_list) # 正常访问
my_list.append(4) # 修改内容,不是赋值
print(my_list)
modify_list() # 正常运行
但是如果你尝试重新赋值(而不是修改内容),仍然会遇到问题:
python复制my_list = [1, 2, 3]
def reassign_list():
print(my_list)
my_list = [4, 5, 6] # 这是赋值操作
reassign_list() # 报错!
当遇到UnboundLocalError时:
使用global变量会比访问局部变量稍慢,因为Python需要查找全局命名空间。在性能关键的代码中,可以考虑:
python复制def fast_func():
local_x = global_x # 复制到局部变量
# 使用local_x进行操作
return local_x
我在一个Web爬虫项目中遇到过这个问题。我们有一个配置字典在模块级别定义,多个函数需要读取和修改这个配置:
python复制config = {
'timeout': 10,
'retries': 3
}
def update_config():
# 这里直接修改config会报错吗?
config['timeout'] = 20 # 不会报错,因为不是直接对config赋值
config = {'timeout': 30} # 这会报错,因为是赋值操作
正确的做法是:
python复制config = {
'timeout': 10,
'retries': 3
}
def safe_update():
global config
config['timeout'] = 20 # 修改内容
# 或者完全替换
new_config = {'timeout': 30, 'retries': 5}
global config
config = new_config # 需要global声明
另一个常见场景是在多线程编程中,多个线程需要访问共享计数器:
python复制from threading import Thread
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1
threads = [Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter) # 可能不是1000000,因为需要线程锁
这个例子还展示了另一个问题:即使使用global,多线程修改共享变量仍然需要额外的同步机制。