讲解

作用域决定一个名字在哪里可见。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 不产生作用域。下一节学习把代码组织进模块。