讲解
作用域决定一个名字在哪里可见。Python 查找变量遵循 LEGB 规则,由内向外依次是:Local(当前函数内)、Enclosing(外层嵌套函数)、Global(模块顶层)、Built-in(内置名字,如 len、print)。函数里读一个变量,Python 按这个顺序找,找到就停;都找不到就报 NameError。函数内可以直接「读」全局变量,但「写」是另一回事——这是作用域最容易出错的地方。
函数内对变量赋值,默认会创建一个同名的局部变量,把全局的「遮住」:count = 0 在顶层,函数里写 count += 1 会报 UnboundLocalError——因为 += 隐含赋值,Python 把整个函数里的 count 都当局部变量,而它又在使用前没赋过值。确实想在函数里改全局变量,需要先声明 global count。不过工程上改全局状态是坏味道,更好的做法是把值作为参数传入、作为返回值传出。
嵌套函数(闭包)场景下,内层函数想修改外层函数的变量用 nonlocal 声明。global 和 nonlocal 都是「我要改外层的绑定」的显式声明,用到它们时都值得停下来想想有没有更清晰的设计。最后提醒:模块顶层定义的变量对整个模块可见,包括模块里所有函数;if、for、while 代码块不产生新作用域——if 分支里定义的变量,分支外照样能用,这和很多语言不同。
示例
# LEGB 查找顺序
level = '全局'
def outer():
level = '外层函数'
def inner():
level = '内层函数'
print('inner 看到:', level)
inner()
print('outer 看到:', level)
outer()
print('顶层看到:', level)
# 函数内读全局:没问题
app_name = 'ohmydocs'
def show_name():
print('读取全局:', app_name)
show_name()
# 函数内改全局:需要 global 声明
visit_count = 0
def record_visit():
global visit_count
visit_count += 1
record_visit()
record_visit()
print('访问次数:', visit_count)
# 不加 global 直接改:UnboundLocalError
def broken_increment():
# visit_count += 1 # 取消注释会报 UnboundLocalError
pass
# nonlocal:内层改外层的变量
def make_counter():
count = 0
def tick():
nonlocal count
count += 1
return count
return tick
counter = make_counter()
print('闭包计数:', counter(), counter(), counter())
# if/for 不产生新作用域
if True:
message = '分支里定义的'
for i in range(3):
pass
print('分支外可见:', message, ',循环变量残留:', i)
常见坑
- 函数里 count += 1 报 UnboundLocalError:赋值让 Python 把它当局部变量,而赋值前就被读取了。要么 global 声明,要么改成参数传入、返回值传出(更推荐)。
- 滥用 global:全局可变状态让程序难以推理和测试。看到 global 先想能不能用返回值或类来组织状态。
- 以为 if/for 有独立作用域:循环结束后循环变量 i 还在,分支里定义的变量外面可见。别依赖块级作用域,Python 没有。
- 变量名遮住内置函数:在函数里定义 max = 100,之后同作用域内 max([1, 2]) 就坏了——LEGB 里局部优先于内置。
小结
查找顺序 LEGB;读全局随意、改全局要 global、改外层用 nonlocal;if/for 不产生作用域。下一节学习把代码组织进模块。