一步一步教你認識Python閉包

在「python之禪」公眾號中曾經介紹過兩篇關於函數的文章,第一篇是 關於 Python 函數是第一類對象,第二篇是關於 Lambda 函數,今天來說說 Python 閉包。

什麼是閉包?閉包有什麼用?為什麼要用閉包?今天我們就帶著這3個問題來一步一步認識閉包。

閉包和函數緊密聯繫在一起,介紹閉包前有必要先介紹一些背景知識,諸如嵌套函數、變數的作用域等概念

作用域

作用域是程序運行時變數可被訪問的範圍,定義在函數內的變數是局部變數,局部變數的作用範圍只能是函數內部範圍內,它不能在函數外引用。

定義在模塊最外層的變數是全局變數,它是全局範圍內可見的,當然在函數裡面也可以讀取到全局變數的。例如:

num = 10 # 全局作用域變數ndef foo():n print(num) # 10n

而在函數外部則不可以訪問局部變數。例如:

def foo():n num = 10nprint(num) # NameError: name num is not definedn

嵌套函數

函數不僅可以定義在模塊的最外層,還可以定義在另外一個函數的內部,像這種定義在函數裡面的函數稱之為嵌套函數(nested function)例如:

def print_msg():n # print_msg 是外圍函數n msg = "zen of python"nn def printer():n # printer是嵌套函數n print(msg)n printer()n# 輸出 zen of pythonnprint_msg()n

對於嵌套函數,它可以訪問到其外層作用域中聲明的非局部(non-local)變數,比如代碼示例中的變數 msg 可以被嵌套函數 printer 正常訪問。

那麼有沒有一種可能即使脫離了函數本身的作用範圍,局部變數還可以被訪問得到呢?答案是閉包

什麼是閉包

函數身為第一類對象,它可以作為函數的返回值返回,現在我們來考慮如下的例子:

def print_msg():n # print_msg 是外圍函數n msg = "zen of python"n def printer():n # printer 是嵌套函數n print(msg)n return printernnanother = print_msg()n# 輸出 zen of pythonnanother()n

這段代碼和前面例子的效果完全一樣,同樣輸出 "zen of python"。不同的地方在於內部函數 printer 直接作為返回值返回了。

一般情況下,函數中的局部變數僅在函數的執行期間可用,一旦 print_msg() 執行過後,我們會認為 msg變數將不再可用。然而,在這裡我們發現 print_msg 執行完之後,在調用 another 的時候 msg 變數的值正常輸出了,這就是閉包的作用,閉包使得局部變數在函數外被訪問成為可能。

看完這個例子,我們再來定義閉包,維基百科上的解釋是:

在計算機科學中,閉包(Closure)是詞法閉包(Lexical Closure)的簡稱,是引用了自由變數的函數。這個被引用的自由變數將和這個函數一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。

這裡的 another 就是一個閉包,閉包本質上是一個函數,它有兩部分組成,printer 函數和變數 msg。閉包使得這些變數的值始終保存在內存中。

閉包,顧名思義,就是一個封閉的包裹,裡面包裹著自由變數,就像在類裡面定義的屬性值一樣,自由變數的可見範圍隨同包裹,哪裡可以訪問到這個包裹,哪裡就可以訪問到這個自由變數。

為什麼要使用閉包

閉包避免了使用全局變數,此外,閉包允許將函數與其所操作的某些數據(環境)關連起來。這一點與面向對象編程是非常類似的,在面對象編程中,對象允許我們將某些數據(對象的屬性)與一個或者多個方法相關聯。

一般來說,當對象中只有一個方法時,這時使用閉包是更好的選擇。來看一個例子:

def adder(x):n def wrapper(y):n return x + yn return wrappernnadder5 = adder(5)n# 輸出 15nadder5(10)n# 輸出 11nadder5(6)n

這比用類來實現更優雅,此外裝飾器也是基於閉包的一中應用場景。

所有函數都有一個 __closure__屬性,如果這個函數是一個閉包的話,那麼它返回的是一個由 cell 對象 組成的元組對象。cell 對象的cell_contents 屬性就是閉包中的自由變數。

>>> adder.__closure__n>>> adder5.__closure__n(<cell at 0x103075910: int object at 0x7fd251604518>,)n>>> adder5.__closure__[0].cell_contentsn5n

這解釋了為什麼局部變數脫離函數之後,還可以在函數之外被訪問的原因的,因為它存儲在了閉包的 cell_contents中了。

推薦閱讀:

入門教程沒有告訴你的sqlalchemy常用操作
跟黃哥學python之類__call__方法
Python 3 實現 Markdown 解析器
量化策略系列教程:13布林強盜系統
Python 抽取word文檔中的文本。

TAG:Python | Python入门 | Python教程 |