由於 Python 中的函數也是物件,因此常常會出現在函數內定義函數、回傳函數的情形。若此時內部函數引用外部函數的區域變數,情況就會變得些微複雜,我們稱之為「閉包(Closure)」。
但只要理解以下 Python 的原理,閉包也能很好理解。
四個原理及示例
一、函數結束時區域變數不會直接被回收,而是等到無人參考。
實際上,被參考的外層變數會被綁到內層變數的 __closure__
上。這個特性讓我們可以有閉包這種酷酷的操作。
二、呼叫函式時,會創造出一個命名空間
執行 outer 而產生一個命名空間。而因為 inner 參考至它,所以 outer 的命名空間不會被回收掉。 1
2
3
4
5
6
7
8
9def outer():
1 owo =
def inner():
print(owo)
return inner
...
func = outer()
func()
1
每次執行函式,都會創造出一個全新的命名空間。這裡要用 nonlocal
是因為,除非加上 nonlocal
關鍵字,外部名稱是唯讀的。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def outer():
0 cnt =
def inner():
nonlocal cnt
1 cnt +=
print(cnt)
return inner
...
f1 = outer()
f2 = outer()
f1(), f1(), f1(), f2(), f2(), f2()
1
2
3
1
2
3
三、作用域是在哪裡 def
決定的,而非在哪裡被呼叫
作用域和搜尋順序關乎於寫在哪裡,而非在哪裡被呼叫:
1 | 'starbuststream' owo = |
四、呼叫函數時,會參考至當前時間的命名空間,和宣告時沒關係。
呼叫 f()
時,該函數會以當前的命名空間狀態為主。此時執行完 for
迴圈,名稱 i
在全域中指向 2
,因此函數參考外部名稱 i
時會讀取到 2
。
注意,Python 的迴圈沒有獨立的命名空間,for i in range(3)
在全域新增了名稱 i
。
1 | func_list = [] |
解決辦法是在外部再包一層函數。利用迴圈執行 outer
,製造出許多獨立的命名空間。每個命名空間裡都會有自己的名稱 j
,分別指向物件 0
、1
、2
:
1 | func_list = [] |
寫成參數有相同療效:
1 | func_list = [] |
另外這裡有一個雷點,以下的程式碼看似可以正常的運作,其實是誤打誤撞:
1 | func_list = [] |
由於使用了 for
,i
在每次迴圈被指向 0
、1
、2
,而執行 func_list[i]()
時,函數又會參考至當前的命名空間,造成了這樣的笑話。若把下面的迴圈改成 j
,就會印出 2 2 2
了。
應用
這項技巧經常用在
- 批量產生函數,如按鈕的 handler
- 裝飾器(Decorator)
以帶參數的裝飾器做示例:
1 | def deco_fac(message): |