第一章:非局部變量的誕生史——Python 閉包的“補完計劃”
1.1 Python 2 的悲情時代:沒有 nonlocal 的閉包殘缺之痛
# Python 2 時代開發者內心的吶喊
def counter():
count = 0
def inc():
count += 1 # UnboundLocalError!靈魂無法綁定!
return count
return inc
只能用以下“邪道”繞過:
- 可變對象容器(list/dict)作弊
- 類包裝
- getattr 黑魔法
1.2 PEP 3104 —— 非局部變量的救世主降臨
2006 年,Guido 親自批准 PEP 3104,Python 3.0 引入 nonlocal,正式宣告: “閉包不再是殘缺的藝術!”
# Python 3 的新生
def counter():
count = 0
def inc():
nonlocal count # ← 靈魂綁定成功
count += 1
return count
return inc
從此,函數式編程在 Python 真正擁有了完整的靈魂。
第二章:三界法則終極對比表(必背)
|
關鍵字
|
綁定目標層級
|
查找順序
|
可讀
|
可寫需要聲明
|
典型錯誤場景
|
推薦使用場景
|
|
local
|
當前函數
|
L
|
√
|
×
|
無
|
臨時變量
|
|
nonlocal
|
最近的外層函數(非全局)
|
E
|
√
|
√
|
無外層/SyntaxError
|
閉包狀態、裝飾器、狀態機
|
|
global
|
模塊最頂層
|
G
|
√
|
√
|
忘記聲明 → 局部影子
|
配置、單例、跨模塊共享
|
記憶口訣:
“要讀就往外找,要寫就先報名:nonlocal 報名找爹,global 報名找祖宗。”
第三章:底層真相——cell 與 closure 的內存之線(附圖解)
def outer():
x = "記憶之種"
def inner():
nonlocal x
x += "✨"
print(x)
print(inner.__code__.co_freevars) # ('x',)
print(inner.__closure__[0].cell_contents) # 記憶之種
return inner
f = outer()
f() # 記憶之種✨
f() # 記憶之種✨✨
內存結構圖(文字版):
outer 函數棧幀 (已銷燬)
│
└── cell 對象 ←──────┐
│
inner 函數對象 │
├── __code__
│ │ └── co_freevars = ('x',)
└── __closure__ ───┘
│
└── cell_contents = "記憶之種✨✨"
這就是非局部變量的真正形態:一個被外層函數“遺贈”的 cell 對象,內層函數通過 __closure__ 元組永久持有它的引用,從而實現“靈魂永存”。
第四章:靈魂撕裂的十大陷阱(血淚史)
4.1 最經典的 UnboundLocalError 靈魂撕裂
def bug():
print(count) # 我想讀外層的 count!
nonlocal count # SyntaxError: no binding for nonlocal 'count' found
4.2 嵌套太深找不到爹
def level1():
x = 1
def level2():
def level3():
nonlocal x # 找到 level1 的 x
x += 1
nonlocal 會一直往外找,直到找到第一個有這個名字的層級,不會跳過。
4.3 晚綁定詛咒(Late Binding)——閉包經典之痛
def traps():
funcs = []
for i in range(3):
def f(): nonlocal i; print(i) # 錯!i 沒有在外層綁定
funcs.append(f)
return funcs
正確姿勢:
def correct():
funcs = []
for i in range(3):
def f(i=i): # 默認參數立即綁定
print(i)
funcs.append(f)
# 或者
for i in range(3):
def f():
nonlocal i # 錯!循環變量不能 nonlocal
第五章:非局部變量在裝飾器中的九九八十一變
5.1 帶狀態的裝飾器(最常見)
def once_per_n_seconds(n):
last_called = 0
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_called
now = time.time()
if now - last_called < n:
return
last_called = now
return func(*args, **kwargs)
return wrapper
return decorator
5.2 計數裝飾器、緩存裝飾器、retry 裝飾器全部依賴 nonlocal
5.3 終極形態:帶配置的通用狀態裝飾器
def stateful(**state_defaults):
def decorator(func):
state = dict(state_defaults)
def wrapper(*args, **kwargs):
nonlocal state
# 可以動態注入 state 到 kwargs,或者直接操作
return func(state, *args, **kwargs)
wrapper.reset = lambda: state.update(state_defaults)
wrapper.state = state
return wrapper
return decorator
第六章:狀態機(FSM)的靈魂實現——非局部的最高境界
def traffic_light():
state = "紅燈"
def next():
nonlocal state
if state == "紅燈":
state = "綠燈"
elif state == "綠燈":
state = "黃燈"
elif state == "黃燈":
state = "紅燈"
return state
def current():
return state
return current, next
get_state, change = traffic_light()
print(change(), change(), change()) # 綠燈 黃燈 紅燈
這種寫法比類實現更輕量、更函數式,在遊戲 AI、協議解析器中廣泛使用。
第七章:異步與生成器中的時空穿梭
7.1 異步狀態機
async def retry_on_fail(max_retries=3):
attempt = 0
async def wrapper(coro):
nonlocal attempt
while attempt < max_retries:
try:
return await coro
except Exception as e:
attempt += 1
if attempt >= max_retries:
raise
await asyncio.sleep(2 ** attempt)
return wrapper
7.2 生成器中的非局部狀態
def averager():
total = 0
count = 0
def add(value):
nonlocal total, count
total += value
count += 1
return total / count
return add
avg = averager()
print(avg(10), avg(20), avg(30)) # 10.0 15.0 20.0
第八章:大型項目實戰案例(真實大廠級)
8.1 GUI 事件計數器(Tkinter/PyQt)
def create_button_with_counter(root):
clicked = 0
def on_click():
nonlocal clicked
clicked += 1
label.config(text=f"點擊了 {clicked} 次")
button = Button(root, text="點我", command=on_click)
8.2 遊戲引擎中的角色狀態系統
def create_player(name, hp=100):
health = hp
mana = 50
def damage(amount):
nonlocal health
health = max(0, health - amount)
def heal(amount):
nonlocal health, mana
if mana >= amount // 2:
mana -= amount // 2; health += amount
def status():
return f"{name}: HP={health} MP={mana}"
return damage, heal, status
8.3 深度學習訓練過程中的進度記錄器(真實訓練框架常用模式)
def make_trainer(model):
best_loss = float('inf')
patience = 10
wait = 0
def on_epoch_end(epoch, loss):
nonlocal best_loss, wait
if loss < best_loss:
best_loss = loss
wait = 0
torch.save(model.state_dict(), "best.pt")
else:
wait += 1
if wait >= patience:
print("Early stopping!")
return True # 停止信號
return False
return on_epoch_end