一个让我在代码审查时社死的经历
去年公司做代码审查,我提交了一段这样的代码:
csharp
counter = 0
def increment():
counter += 1
return counter
def outer():
x = 10
def inner():
x += 1
return x
return inner()
审查官看了一眼,说:"你这两个函数都会报错,知道为什么吗?"
我当时脑子一热,脱口而出:"我知道,第一个要加global,第二个要加nonlocal。"
"那你加了没?"
"呃......忘了。"
审查官笑了笑:"你这不是忘了,你是分不清什么时候用global,什么时候用nonlocal。"
他说得对。我当时确实分不清。这两个关键字看起来差不多,都是在函数里声明"这个变量不是我本地的",但具体怎么用,脑子里是一团浆糊。
今天我就把这两个关键字的区别彻底讲清楚。看完之后,你绝对不会再搞混。
先看一个最简单的对比
python
# global 的例子
x = 10 # 全局变量
def func1():
global x
x = 20 # 修改全局的x
func1()
print(x) # 20
# nonlocal 的例子
def outer():
y = 10 # 外层函数的变量
def inner():
nonlocal y
y = 20 # 修改外层函数的y
inner()
print(y) # 20
outer()
从代码上看,区别很明显:
global用在函数内部 ,目标是全局作用域的变量nonlocal用在嵌套函数 内部,目标是外层函数的变量
但光看这个还不够。我们深入拆解一下。
global:走出函数,走到文件最外层
先看一个会报错的例子:
ini
name = "张三"
def change():
name = "李四" # 这是创建了一个新的局部变量,不是修改全局的
change()
print(name) # 输出"张三"------没变!
上面的代码不会报错,但是达不到你想要的效果。因为name = "李四"在函数内部创建了一个同名的局部变量,外面的name根本没动过。
如果加上global:
ini
name = "张三"
def change():
global name
name = "李四" # 现在修改的是全局变量
change()
print(name) # 输出"李四"
global的作用就是告诉Python:"别在函数里创建新变量,直接用外面的那个。"
global的核心规则:
- 只能在函数内部使用(模块顶层不需要,因为顶层本来就是全局作用域)
- 声明的变量名必须是全局作用域里已经存在的,或者你准备在全局创建它
- 一个函数里可以有多个
global声明:global a, b, c
global的典型应用场景
场景1:修改配置变量
ini
DEBUG = False
def enable_debug():
global DEBUG
DEBUG = True
def disable_debug():
global DEBUG
DEBUG = False
场景2:计数器
python
call_count = 0
def track_call():
global call_count
call_count += 1
print(f"这个函数被调用了{call_count}次")
场景3:在函数内部创建全局变量
csharp
def create_global():
global new_var
new_var = "我是在函数里创建的全局变量"
create_global()
print(new_var) # 正常输出
这种情况比较少见,但语法上是允许的。
global 容易踩的坑
坑1:在global声明之前使用变量
ini
x = 10
def bad():
print(x) # 这里想读全局x
global x # 但是global声明应该在前面
x = 20
bad()
这会报语法错误:SyntaxError: name 'x' is used prior to global declaration
正确写法:global声明要放在函数的最前面(在用到这个变量之前)。
坑2:在for/if里用global
go
x = 10
def func():
for i in range(3):
global x # 语法上可以,但没必要放在循环里
x = i
func()
print(x) # 2
global声明应该放在函数顶部,不要写在循环或条件语句里。虽然语法允许,但会让人困惑。
坑3:以为global可以跨文件
ini
# file1.py
x = 10
# file2.py
from file1 import x
def change():
global x # 这个x并不是file1里的x!
x = 20
global只在当前模块(当前文件)的全局作用域里生效。如果你从别的模块导入了一个变量,修改它不会影响原模块。
跨文件共享状态应该用模块对象(import file1; file1.x = 20),而不是global。
nonlocal:走出内层函数,但只走到外层函数
nonlocal是Python 3才引入的。在Python 2里,嵌套函数想修改外层函数的变量,只能把变量做成列表或字典来绕过限制。
看一个会报错的例子:
csharp
def outer():
count = 0
def inner():
count += 1 # 报错!count被视为inner的局部变量
return count
return inner()
加上nonlocal:
python
def outer():
count = 0
def inner():
nonlocal count # 声明:这个count来自外层函数
count += 1
return count
return inner()
print(outer()) # 1
nonlocal的核心规则:
- 只能在嵌套函数(函数里面定义的函数)内部使用
- 声明的变量必须在外层函数的局部作用域里已经存在
- 不能用来声明全局变量(会报语法错误)
- 不能跳过外层直接修改更外层的变量,它只找最近的一层
nonlocal的典型应用场景
场景1:闭包里的计数器
python
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
这是闭包的标准写法:外层函数创建一个变量,内层函数通过nonlocal修改它。
场景2:装饰器里维护状态
python
def count_calls(func):
call_count = 0
def wrapper(*args, **kwargs):
nonlocal call_count
call_count += 1
print(f"{func.__name__}被调用了{call_count}次")
return func(*args, **kwargs)
return wrapper
@count_calls
def say_hello():
print("你好")
say_hello() # say_hello被调用了1次
say_hello() # say_hello被调用了2次
场景3:多层嵌套
python
def outer():
x = "outer"
def middle():
x = "middle" # 这是middle的局部变量
def inner():
nonlocal x # 这个x指向谁?
x = "inner"
inner()
print(x) # 输出"inner"
middle()
print(x) # 输出"outer"------外层的x没有被影响
outer()
nonlocal查找规则:从当前函数(inner)的直接外层 开始找,找到的第一个同名变量就是目标。这里找到的是middle里的x,不是outer里的。
如果middle里没有定义x,nonlocal会继续往outer里找。如果都找不到,报语法错误。
一张对比表,一目了然
| 特性 | global | nonlocal |
|---|---|---|
| 用在哪里 | 任何函数内部 | 仅限嵌套函数内部 |
| 目标作用域 | 全局作用域 | 外层函数的局部作用域 |
| 目标变量必须存在? | 否(可以在函数里创建新的全局变量) | 是(必须已经在外层函数里定义) |
| 能用在模块顶层吗? | 不能(顶层不需要) | 不能(顶层没有外层函数) |
| 能跨文件吗? | 不能(只在当前模块有效) | 不适用 |
| Python版本 | 所有版本 | Python 3+ |
三个容易混淆的细节
细节1:nonlocal不能声明不存在的变量
python
def outer():
def inner():
nonlocal x # SyntaxError: no binding for nonlocal 'x' found
x = 10
nonlocal要求变量已经存在于外层作用域。这和global不同,global可以在函数里创建新的全局变量。
细节2:在同一个作用域里,global和nonlocal不能混用
python
x = 10
def outer():
x = 20
def inner():
global x # 指向全局的x(值为10)
nonlocal x # SyntaxError! 不能同时声明
一个变量要么是全局的,要么是外层函数的,不能同时是两者。
细节3:nonlocal只能往上找一层吗?
很多人以为nonlocal只能找直接外层,其实不是。它会一直往上找,直到找到最近的匹配:
python
def outer():
x = "outer"
def middle():
# middle没有定义x
def inner():
nonlocal x # 从middle开始找,找不到,继续往外找,找到outer里的x
x = "inner changed"
inner()
middle()
print(x) # "inner changed"
outer()
nonlocal会沿着嵌套层次一层层往上找,直到找到目标变量。但如果到了全局还没找到,就会报错(nonlocal不会到全局去找)。
实战:用闭包造一个"带状态的函数"
这是一个结合了nonlocal的经典例子:
python
def create_account(initial_balance=0):
balance = initial_balance
transactions = []
def deposit(amount):
nonlocal balance
balance += amount
transactions.append(f"存入{amount}")
return balance
def withdraw(amount):
nonlocal balance
if amount > balance:
raise ValueError("余额不足")
balance -= amount
transactions.append(f"取出{amount}")
return balance
def get_balance():
return balance
def get_transactions():
return transactions.copy()
return deposit, withdraw, get_balance, get_transactions
deposit, withdraw, get_balance, get_transactions = create_account(100)
deposit(50) # 150
withdraw(30) # 120
print(get_balance()) # 120
print(get_transactions()) # ['存入50', '取出30']
这里用nonlocal让deposit和withdraw能修改外层函数的balance变量。
如果不加nonlocal,balance += amount会在deposit内部创建局部变量balance,外面的balance不会变。
为什么Python要分这两个关键字?
一个常见的问题是:"为什么不统一用outer或者scope之类的关键字,非要分global和nonlocal?"
原因有两个:
1. 语义不同
global是从当前作用域直接跳到模块顶层,是一种"跳跃式"的访问。 nonlocal是沿着嵌套关系逐层往上找,是一种"爬楼梯式"的访问。
这两种行为不一样,用不同的关键字能清晰地表达意图。
2. 安全性
nonlocal不能用于全局变量,这防止了你在嵌套函数里无意中修改了全局状态。如果你确实想改全局变量,必须明确使用global。
这种设计强迫程序员显式地声明"我知道这个变量是共享的,我负责"。
快速判断该用哪个
如果你需要修改一个变量,问自己三个问题:
问题1:这个变量在哪里定义的?
- 在函数外面定义的 → 用
global - 在外层函数里定义的 → 用
nonlocal - 在内层函数里定义的 → 不需要任何关键字(它就是局部变量)
问题2:我现在在哪?
- 在一个普通函数里 → 只能选
global(如果确实需要改全局变量) - 在一个嵌套函数里 → 可能用
global也可能用nonlocal,取决于目标变量在哪
问题3:如果不确定,先不加关键字,看报错信息
- 报错说
local variable referenced before assignment→ 需要用global或nonlocal - 报错说
no binding for nonlocal→ 说明变量不在外层函数里,试试global - 报错说
no binding for global→ 说明变量不在全局里,试试nonlocal
一个终极测试
猜猜下面这段代码的输出是什么?
python
x = "global"
def outer():
x = "outer"
def middle():
x = "middle"
def inner():
nonlocal x
x = "inner"
print(f"inner里: {x}")
inner()
print(f"middle里: {x}")
middle()
print(f"outer里: {x}")
outer()
print(f"全局: {x}")
答案:
sql
inner里: inner
middle里: inner
outer里: outer
全局: global
原因:
inner里的nonlocal x找到了middle里的x(最近的外层),把它改成"inner"outer里的x没有被影响,因为nonlocal只往上找到最近的那一层就停了- 全局的
x完全不受影响
如果你能完全说清楚这个输出,说明你已经彻底掌握了global和nonlocal的区别。
最后一句总结
global:我要修改全局变量,跟当前函数之外的文件顶层对话nonlocal:我要修改外层函数的变量,跟外层的函数对话
记住这个简单的场景选择 :如果变量在函数外面(文件顶层),用global。如果变量在外层函数里,用nonlocal。
就这一句话,够用了。