草根學Python(十五) 閉包(解決一個需求瞭解閉包流程)

兩點水發表於2019-02-22

網路上介紹 Python 閉包的文章已經很多了,本文將通過解決一個需求問題來了解閉包。

這個需求是這樣的,我們需要一直記錄自己的學習時間,以分鐘為單位。就好比我學習了 2 分鐘,就返回 2 ,然後隔了一陣子,我學習了 10 分鐘,那麼就返回 12 ,像這樣把學習時間一直累加下去。

面對這個需求,我們一般都會建立一個全域性變數來記錄時間,然後用一個方法來新增每次的學習時間,通常都會寫成下面這個形式:

time = 0

def insert_time(min):
    time = time + min
    return  time

print(insert_time(2))
print(insert_time(10))
複製程式碼

認真想一下,會不會有什麼問題呢?

其實,這個在 Python 裡面是會報錯的。會報如下錯誤:

UnboundLocalError: local variable `time` referenced before assignment
複製程式碼

那是因為,在 Python 中,如果一個函式使用了和全域性變數相同的名字且改變了該變數的值,那麼該變數就會變成區域性變數,那麼就會造成在函式中我們沒有進行定義就引用了,所以會報該錯誤。

如果確實要引用全域性變數,並在函式中對它進行修改,該怎麼做呢?

我們可以使用 global 關鍵字,具體修改如下:

time = 0


def insert_time(min):
    global  time
    time = time + min
    return  time

print(insert_time(2))
print(insert_time(10))
複製程式碼

輸出結果如下:

2
12
複製程式碼

可是啊,這裡使用了全域性變數,我們在開發中能儘量避免使用全域性變數的就儘量避免使用。因為不同模組,不同函式都可以自由的訪問全域性變數,可能會造成全域性變數的不可預知性。比如程式設計師甲修改了全域性變數 time 的值,然後程式設計師乙同時也對 time 進行了修改,如果其中有錯誤,這種錯誤是很難發現和更正的。

全域性變數降低了函式或模組之間的通用性,不同的函式或模組都要依賴於全域性變數。同樣,全域性變數降低了程式碼的可讀性,閱讀者可能並不知道呼叫的某個變數是全域性變數。

那有沒有更好的方法呢?

這時候我們使用閉包來解決一下,先直接看程式碼:

time = 0


def study_time(time):
    def insert_time(min):
        nonlocal  time
        time = time + min
        return time

    return insert_time


f = study_time(time)
print(f(2))
print(time)
print(f(10))
print(time)
複製程式碼

輸出結果如下:

2
0
12
0
複製程式碼

這裡最直接的表現就是全域性變數 time 至此至終都沒有修改過,這裡還是用了 nonlocal 關鍵字,表示在函式或其他作用域中使用外層(非全域性)變數。那麼上面那段程式碼具體的執行流程是怎樣的。我們可以看下下圖:

Python 閉包解決

這種內部函式的區域性作用域中可以訪問外部函式區域性作用域中變數的行為,我們稱為: 閉包。更加直接的表達方式就是,當某個函式被當成物件返回時,夾帶了外部變數,就形成了一個閉包。k

閉包避免了使用全域性變數,此外,閉包允許將函式與其所操作的某些資料(環境)關連起來。而且使用閉包,可以使程式碼變得更加的優雅。而且下一篇講到的裝飾器,也是基於閉包實現的。

到這裡,就會有一個問題了,你說它是閉包就是閉包了?有沒有什麼辦法來驗證一下這個函式就是閉包呢?

有的,所有函式都有一個 __closure__ 屬性,如果函式是閉包的話,那麼它返回的是一個由 cell 組成的元組物件。cell 物件的 cell_contents 屬性就是儲存在閉包中的變數。

我們列印出來體驗一下:

time = 0


def study_time(time):
    def insert_time(min):
        nonlocal  time
        time = time + min
        return time

    return insert_time


f = study_time(time)
print(f.__closure__)
print(f(2))
print(time)
print(f.__closure__[0].cell_contents)
print(f(10))
print(time)
print(f.__closure__[0].cell_contents)
複製程式碼

列印的結果為:

(<cell at 0x0000000000410C48: int object at 0x000000001D6AB420>,)
2
0
2
12
0
12
複製程式碼

從列印結果可見,傳進來的值一直儲存在閉包的 cell_contents 中,因此,這也就是閉包的最大特點,可以將父函式的變數與其內部定義的函式繫結。就算生成閉包的父函式已經釋放了,閉包仍然存在。

閉包的過程其實好比類(父函式)生成例項(閉包),不同的是父函式只在呼叫時執行,執行完畢後其環境就會釋放,而類則在檔案執行時建立,一般程式執行完畢後作用域才釋放,因此對一些需要重用的功能且不足以定義為類的行為,使用閉包會比使用類佔用更少的資源,且更輕巧靈活。

歡迎開啟微信掃一掃,關注微信公眾號:

微信公眾號

相關文章