閉包是python中內部函數捕獲外部函數作用域變量并持續訪問的機制。具體來說,閉包是一個函數加上其引用環境,即使外部函數執行完畢,內部函數仍能記住并訪問外部變量。例如,在outer函數中定義的inner函數可以訪問outer的變量x,即使outer已執行完成。閉包變量是后期綁定(late binding),即調用時才查找變量當前值,這可能導致多個閉包引用同一個變量而得到相同最終值的問題,如make_multipliers例子中所有Lambda都返回8。解決方法是通過默認參數立即綁定值。閉包變量屬于嵌套函數外層作用域(e層級),讀取無需聲明,修改則需使用nonlocal關鍵字。閉包常用于狀態保持、封裝數據和裝飾器,但也需注意內存釋放、變量綁定問題及多層嵌套帶來的效率影響。可通過__closure__屬性檢查閉包內容。掌握閉包的作用域規則與綁定機制是寫出穩定代碼的關鍵。
在python中,閉包是一個函數加上它所處環境的引用。說得更具體一點,就是內部函數記住了外部函數作用域中的變量值,即使外部函數已經執行完畢,這些變量仍然可以被訪問。
這種機制常用于裝飾器、回調函數等場景,理解閉包變量的綁定機制,能幫助我們寫出更穩定、可預測的代碼。
什么是閉包?
閉包(Closure)指的是一個函數捕獲了其定義時所在的環境變量,并且即使這個環境不再存在,它也能繼續訪問這些變量。
舉個簡單例子:
立即學習“Python免費學習筆記(深入)”;
def outer(x): def inner(): print(x) return inner closure = outer(10) closure() # 輸出 10
在這個例子中,inner 是一個閉包,因為它“記住”了 outer 函數中的變量 x。即使 outer 已經執行完,返回的 inner 還是可以訪問到 x 的值。
閉包變量是按引用還是按值綁定的?
這是很多人容易混淆的地方:閉包變量在 Python 中是后期綁定的(late binding),也就是說,它不是在定義的時候固定住變量的值,而是在調用時才去查找變量的當前值。
來看一個經典例子:
def make_multipliers(): return [lambda x: x * i for i in range(5)] for m in make_multipliers(): print(m(2))
你可能會以為輸出是 0 2 4 6 8,但實際輸出是 8 8 8 8 8。
為什么?因為在列表推導式中,每個 lambda 都引用了同一個變量 i,而當 make_multipliers() 執行完后,i 最終停留在了 4,所以所有閉包中的 i 都指向最后的值。
要解決這個問題,通常的做法是在定義時立即綁定值,比如通過默認參數“凍結”當前值:
def make_multipliers(): return [lambda x, i=i: x * i for i in range(5)]
這樣每個 lambda 都保存了當時循環中的 i 值。
嵌套函數中變量的作用域規則
在嵌套函數中,Python 使用 LEGB 規則 來查找變量:
- L: Local(本地作用域)
- E: Enclosing(外層嵌套函數作用域)
- G: Global(全局作用域)
- B: Built-in(內置作用域)
閉包變量屬于 E 層級,也就是外層函數作用域中的變量。
如果我們在內層函數中修改這些變量,需要注意以下幾點:
- 如果只是讀取,不需要做任何聲明。
- 如果想修改,需要使用 nonlocal 關鍵字(適用于嵌套函數)或 global(如果變量在全局)。
例如:
def outer(): count = 0 def inner(): nonlocal count count += 1 print(count) return inner counter = outer() counter() # 輸出 1 counter() # 輸出 2
如果沒有 nonlocal,Python 會認為你在給局部變量 count 賦值,從而引發錯誤。
實際應用與注意事項
閉包在很多高級技巧中都有使用,比如:
- 模擬狀態保持(如計數器)
- 封裝數據,避免污染全局空間
- 構造裝飾器邏輯
不過要注意幾個點:
- 閉包會持有對外部變量的引用,可能導致內存無法釋放。
- 多層嵌套函數中,變量查找效率略低(雖然一般影響不大)。
- 循環中生成閉包時,注意 late binding 的問題。
如果你不確定某個變量是否被正確捕獲,可以用 __closure__ 屬性查看閉包內容:
def outer(): x = 10 def inner(): return x return inner f = outer() print(f.__closure__) # 輸出類似 (<cell at 0x...: int object at 0x...>,)
基本上就這些。閉包是個強大又容易出錯的功能,掌握好變量綁定機制和作用域規則,才能用得更穩。