在 Python 中,函式也是一個物件。因此,我們在定義函式時,可以再巢狀定義一個函式,並將該巢狀函式返回,比如:
1 2 3 4 5 6 |
from math import pow def make_pow(n): def inner_func(x): # 巢狀定義了 inner_func return pow(x, n) # 注意這裡引用了外部函式的 n return inner_func # 返回 inner_func |
上面的程式碼中,函式 make_pow
裡面又定義了一個內部函式 inner_func
,然後將該函式返回。因此,我們可以使用 make_pow
來生成另一個函式:
1 2 3 4 5 |
>>> pow2 = make_pow(2) # pow2 是一個函式,引數 2 是一個自由變數 >>> pow2 <function inner_func at 0x10271faa0> >>> pow2(6) 36.0 |
我們還注意到,內部函式 inner_func
引用了外部函式 make_pow
的自由變數 n
,這也就意味著,當函式 make_pow
的生命週期結束之後,n
這個變數依然會儲存在 inner_func
中,它被 inner_func
所引用。
1 2 3 4 5 6 7 |
>>> del make_pow # 刪除 make_pow >>> pow3 = make_pow(3) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'make_pow' is not defined >>> pow2(9) # pow2 仍可正常呼叫,自由變數 2 仍儲存在 pow2 中 81.0 |
像上面這種情況,一個函式返回了一個內部函式,該內部函式引用了外部函式的相關引數和變數,我們把該返回的內部函式稱為閉包(Closure)。
在上面的例子中,inner_func
就是一個閉包,它引用了自由變數 n
。
閉包的作用
- 閉包的最大特點就是引用了自由變數,即使生成閉包的環境已經釋放,閉包仍然存在;
- 閉包在執行時可以有多個例項,即使傳入的引數相同,比如:
1 2 3 4 |
>>> pow_a = make_pow(2) >>> pow_b = make_pow(2) >>> pow_a == pow_b False |
- 利用閉包,我們還可以模擬類的例項。
這裡構造一個類,用於求一個點到另一個點的距離:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from math import sqrt class Point(object): def __init__(self, x, y): self.x, self.y = x, y def get_distance(self, u, v): distance = sqrt((self.x - u) ** 2 + (self.y - v) ** 2) return distance >>> pt = Point(7, 2) # 建立一個點 >>> pt.get_distance(10, 6) # 求到另一個點的距離 5.0 |
用閉包來實現:
1 2 3 4 5 6 7 8 9 |
def point(x, y): def get_distance(u, v): return sqrt((x - u) ** 2 + (y - v) ** 2) return get_distance >>> pt = point(7, 2) >>> pt(10, 6) 5.0 |
可以看到,結果是一樣的,但使用閉包實現比使用類更加簡潔。
常見誤區
閉包的概念很簡單,但實現起來卻容易出現一些誤區,比如下面的例子:
1 2 3 4 5 6 7 |
def count(): funcs = [] for i in [1, 2, 3]: def f(): return i funcs.append(f) return funcs |
在該例子中,我們在每次 for
迴圈中建立了一個函式,並將它存到 funcs
中。現在,呼叫上面的函式,你可能認為返回結果是 1, 2, 3,事實上卻不是:
1 2 3 4 5 6 7 |
>>> f1, f2, f3 = count() >>> f1() 3 >>> f2() 3 >>> f3() 3 |
為什麼呢?原因在於上面的函式 f
引用了變數 i
,但函式 f
並非立刻執行,當 for
迴圈結束時,此時變數 i
的值是3,funcs
裡面的函式引用的變數都是 3,最終結果也就全為 3。
因此,我們應儘量避免在閉包中引用迴圈變數,或者後續會發生變化的變數。
那上面這種情況應該怎麼解決呢?我們可以再建立一個函式,並將迴圈變數的值傳給該函式,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def count(): funcs = [] for i in [1, 2, 3]: def g(param): f = lambda : param # 這裡建立了一個匿名函式 return f funcs.append(g(i)) # 將迴圈變數的值傳給 g return funcs >>> f1, f2, f3 = count() >>> f1() 1 >>> f2() 2 >>> f3() 3 |
小結
- 閉包是攜帶自由變數的函式,即使建立閉包的外部函式的生命週期結束了,閉包所引用的自由變數仍會存在。
- 閉包在執行可以有多個例項。
- 儘量不要在閉包中引用迴圈變數,或者後續會發生變化的變數。