淺析Python閉包
1、什麼是閉包
在介紹閉包概念前,我們先來看一段簡短的程式碼
def sum_calc(*args): def wrapper(): sum = 0 for n in args: sum += n; return sum return wrapper
很顯然,這段程式碼定義了一個名為sum_calc的函式,但和定義的普通函式不同的是這個函式體的內部又定義了一個名為wrapper的函式,並且sum_calc函式的返回值是內部定義wrapper函式。
現在我們開始來呼叫sum_calc函式,看看會出現哪些有趣的事情
>>> f = sum_calc(1, 2, 3, 4, 5) >>> f <function sum_calc.<locals>.wrapper at 0x0000025693AC2D30>
>>> f() 15
從執行結果來看,當我們呼叫sum_calc時,返回的並不是求和結果,而是內部定義的求和函式。
繼續呼叫sum_calc返回函式時,才真正計算出求和的結果。
當我們繼續呼叫一次sum_calc,傳入相同引數
>>> f1 = sum_calc(1, 2, 3, 4, 5) >>> f2 = sum_calc(1, 2, 3, 4, 5)>>> f1 == f2 False
>>> f1()
15
>>> f2()
15
每次呼叫sum_calc,即使傳入相同的引數,兩次返回的物件不同,且f1()和f2()的結果互不影響。
在這個例子中,我們在sum_calc函式中定義的wrapper函式,wrapper函式可以引用外部函式sum_calc的引數和區域性變數,當sum_calc返回函式wrapper時,相關引數儲存在返回的函式中,可以被返回的函式繼續使用,我們把這種情況稱為“閉包”。
參考維基百科,對“閉包”進行更嚴謹的解釋:
在電腦科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函式閉包(function closures),是引用了自由變數
2、閉包陷阱
def my_fun(): fs = [] for i in range(0, 3): def f(): return i*i fs.append(f) return fs
f1, f2, f3 = count()
在上面的例子中,每次迴圈,都建立了一個新的函式,然後,把建立的3個函式都放在列表中返回。
我們可能認為,f1()、f2()、f3()結果是0、1、4,但實際結果是4
>>> f1() 4 >>> f2() 4 >>> f3() 4
這個例子可以說是典型的錯誤使用閉包的案例,my_fun返回的並不是一個閉包函式,而是一個包含三個閉包函式的一個列表。
在返回閉包列表fs之前for迴圈的變數的值已經發生改變了,迴圈內閉包函式僅聲明瞭自己計算方式,並不會立即使用當前i的值進行計算。只有在真正呼叫閉包函式時,才會真正的執行閉包函式內的計算,而此時存放的i的值已經是2,所以f1()、f2()、f3()的結果是4而不是我在之前期待的0、1、4。
經過上面的分析,我們得出下面一個重要的經驗:返回閉包中不要引用任何迴圈變數,或者後續會發生變化的變數。
正確寫法
def my_fun(*args): L = [] for i in range(3): def wrapper(_i = i): return _i * _i L.append(wrapper) return L
3、閉包實現機制
閉包比普通的函式多了一個 __closure__ 屬性,該屬性記錄著自由變數內容。當閉包被呼叫時,系統就會根據該地址找到對應的自由變數,完成整體的函式呼叫
還以 sum_calc() 為例,當其被呼叫時,可以通過 __closure__ 屬性獲取自由變數(也就是程式中的 args引數)內容,例如:
def sum_calc(*args): def wrapper(): sum = 0 for n in args: sum += n; return sum return wrapper
輸出結果:
>>> f1 = sum_calc(1, 2, 3, 4, 5) >>> f1.__closure__ (<cell at 0x0000025693A5E4C0: tuple object at 0x0000025693AE5400>,)
>>> f1.__closure__[0].cell_contents
(1, 2, 3, 4, 5)
可以看到,顯示的內容是一個tuple型別,這就是f1中自由變數args的值。還可以看到,__closure__ 屬性的型別是一個元組,這表明閉包可以支援多個自由變數的形式。