1. 程式人生 > 實用技巧 >淺析Python閉包

淺析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__ 屬性的型別是一個元組,這表明閉包可以支援多個自由變數的形式。