第一章:非局部變量的誕生史——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