之前學 js 的時候第一次見到閉包,當時不甚了了,還爲了應付面試強行記住了一個模棱兩可的「定義」:在函數中嵌套定義函數,而且在外層將內層函數返回,一同返回了外層函數的環境。當時從字面意思以及當時一個經典例子試圖去理解閉包,加之"閉包"這個翻譯也很不容易讓人味出其中的道理,致使對其總感受懵懵懂懂。最近工做須要,用起 python,又遇到閉包,此次看到了一些新奇有趣的資料,這纔算大體把一些字面上的概念(first-class functions,bind,scope等等)貫通在一塊兒,反過來對閉包有了更深的理解。python
引用資料列在最後,十分推薦你們去讀讀。web
計算機中有些英文專業詞彙,字面直譯,不免因缺乏上下文而顯得蒼白拗口,須得多方鋪墊,方能味得古怪下面的原理。閉包(closure)即是一個這樣牽扯了許多上下文的概念,包括編程語言最基本的綁定(binding),環境(environments),變量做用域(scope)以及函數是第一等公民(function as the first-class)等等。面試
在Python中,binding 是編程語言最基本的抽象手法,它將一個值綁定到一個變量上,而且稍後能夠引用或者修改該變量。下面是幾種不一樣層次的綁定,每組語句在運行時將一個名字與對應值綁定到其定義所在的環境中。編程
包括將名字綁定到一塊內存,經過賦值語句實現,固然函數調用時,形參和實參結合也是綁定:bash
In [1]: square = 4
複製代碼
將名字綁定到一組複合運算,即函數定義,利用 def 關鍵字實現:數據結構
In [1]: def square(x):
return x*x
複製代碼
將名字綁定到一個數據集合,即類定義,使用 class 實現:閉包
In [1]: class square:
def __init__(self, x):
self.x = x
def value(self):
return self.x * self.x
複製代碼
依照執行順序,屢次同名綁定,後面會覆蓋前面:app
In [1]: square = 3
In [2]: square
Out[2]: 3
In [3]: def square(x):
...: return x * x
...:
...:
In [4]: square
Out[4]: <function __main__.square(x)>
In [5]: class square:
...: def __init__(self, x):
...: self.x = x
...:
In [6]: square
Out[6]: __main__.square
複製代碼
說這些都是抽象,是由於它們提供了對數據,複合操做或數據集合的封裝手段,即將一個名稱與複雜的數據或邏輯進行捆綁,使調用者不用關心其實現細節,而且以此來做爲構建更復雜的工程的基本元素。能夠說綁定是編程的基石。編程語言
回到本文的主題上來,閉包首先是函數,只不過是一種特殊的函數,至於這個特殊性在哪,等稍後引入更多概念後再進行闡述。函數
scope(做用域),顧名思義,也就是某個binding 能罩多大的範圍,或者說你能夠在多大範圍內訪問一個變量。每一個函數定義會構造一個局部定義域。
python,和大多數編程語言同樣,使用的是靜態做用域(static scoping,有時也稱 lexical scoping)規則。在函數嵌套定義的時候,內層函數內能夠訪問外層函數的變量值。所以你能夠把做用域想象成一個容器,即它是能夠嵌套的,而且內層做用域會擴展外層做用域,而最外層做用域即全局做用域。
上一小節提到了,屢次同名綁定,後面會覆蓋前面,其實有隱含前提:在同一做用域內。若是是嵌套做用域,實際上是隱藏的關係,內層函數的變量定義會遮蔽外層函數同一名字定義,可是在外層做用域中,該變量還是原值:
In [16]: a = 4
In [17]: def outer():
...: a = 5
...: print(a)
...: def inner():
...: a = 6
...: print(a)
...: inner()
...: print(a)
...:
In [18]: outer()
5
6
5
In [19]: print(a)
4
複製代碼
能夠看出,做用域其實也能夠從另外一個角度理解,即咱們在某個環境(environment)中,在肯定一個name binding 值的時候,會從最內層做用域順着往外找,找到的第一個該名字 binding 的對應的值即爲該 name 引用到的值。
須要強調的時候,函數的嵌套定義會引發定義域的嵌套,或者說環境擴展(內層擴展外層)關係。類的定義又稍有不一樣,class 定義會引入新的 namespace,namespace 和 scope 是常拿來對比的概念,但這裏按下不表,感興趣的能夠本身去查查資料。
說到這裏,要提一下,一個常被提及的例子:
In [50]: a = 4
In [51]: def test():
...: print(a) # 這裏應該輸出什麼?
...: a = 5
...:
In [52]: test()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-52-fbd55f77ab7c> in <module>()
----> 1 test()
<ipython-input-51-200f78e91a1b> in test()
1 def test():
----> 2 print(a)
3 a = 5
4
UnboundLocalError: local variable 'a' referenced before assignment
複製代碼
想象中,上面 print
處應該輸出 4 或者 5 纔對,爲何會報錯呢?這是由於 test
函數在定義時候,分詞器會掃一遍 test 函數定義中的全部 token,看到賦值語句 a=5
的存在,就會明確 a
是一個局部變量,所以不會輸出4。 而在執行到 print(a)
的時候,在局部環境中,a
還未被binding,所以會報 UnboundLocalError
。
稍微探究一下,雖然 python 是解釋執行的,即輸入一句,解釋一句,執行一句。可是對於代碼塊(即頭部語句,冒號與其關聯的縮進塊所構成的複合語句(compound sentences),常見的有函數定義,類定義,循環語句等等)來講,仍是會總體先掃一遍的。
通常來講,組成編程語言的元素,如變量,函數和類,會被設定不一樣的限制,而具備最少限制的元素,被咱們成爲該編程語言中的一等公民。而一等公民最多見的特權有:
套用到python中的函數來講來講,即一個函數能夠被賦值給某個變量,能夠被函數接收和返回,能夠定義在其餘函數中(即嵌套定義):
In [32]: def test():
...: print('hello world')
...:
In [33]: t = test # 賦值給變量
In [34]: t()
hello world
In [35]: def wrapper(func):
...: print('wrapper')
...: func()
...:
In [36]: wrapper(t) # 做爲參數傳遞
wrapper
hello world
In [37]: def add_num(a):
...: def add(b): # 嵌套定義
...: return a + b
...: return add # 做爲函數的返回值
...:
...:
In [38]: add5 = add_num(5)
In [39]: add5(4)
Out[39]: 9
複製代碼
並非在全部語言中,函數都是一等公民,好比 Java,上面四項權利 Java 中的函數全都沒有。
在這裏,可以操做其餘函數的函數(即以其餘函數做爲參數或者返回值的函數),叫作高階函數。高階函數使得語言的表達能力大大加強,但同時,也不容易用好。
每一個函數調用,會在環境中產生一個棧幀(frame),而且在棧幀中會進行一些綁定,而後壓入函數調用棧中。在函數調用結束時,棧幀會被彈出,其中所進行的綁定也被解除,即垃圾回收,局部做用域也隨之消亡。
In [47]: def test():
...: x = 4
...: print(x)
...:
In [48]: test()
4
In [49]: x
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-49-6fcf9dfbd479> in <module>()
----> 1 x
NameError: name 'x' is not defined
複製代碼
即在調用結束後,局部定義的變量 x
在外邊是訪問不到的。可是如以前例子 中,返回的 add
函數卻引用了已經調用結束的 add_num
中的變量 a
,怎麼解釋這種現象呢?能夠記住一條,也是以前提到過的:
函數嵌套定義時,內部定義的函數所在的環境會自動擴展其定義所在環境
複製代碼
所以在外部函數返回後,返回的內部函數依然維持了其定義時的擴展環境,也能夠理解爲因爲內部函數引用的存在,外部函數的環境中全部的 bindings 並無被回收。
千呼萬喚始出來,覺得高潮其實已結束。
閉包就是創建在前面的這些概念上的,上面提到的某個例子:
In [37]: def add_num(a):
...: def add(b): # 嵌套定義
...: return a + b
...: return add # 做爲函數的返回值
...:
...:
In [38]: add5 = add_num(5)
In [39]: add5(4)
Out[39]: 9
複製代碼
其實就是閉包。撿起以前伏筆,給出我對閉包的一個理解:它是一種高階函數,而且外層函數(例子中的*add_num
)將其內部定義的函數(add
)做爲返回值返回,同時因爲返回的內層函數擴展了外層函數的環境(environment),也就是對其產生了一個引用,那麼在調用返回的內部函數(add5
*)的時候,可以引用到其(add
)定義時的外部環境(在例子中,即 a
的值)。
說了這麼多,其實只是在邏輯層面或者說抽象層面去解釋閉包是什麼,跟哪些概念糾纏在一塊兒。但這些其實都不本質,或者說依然是空中樓閣,若是想要真正理解閉包,能夠去詳細瞭解下 python 的解釋執行機制,固然,那就是編譯原理範疇的東西了。