1. 程式人生 > >Python閉包詳解

Python閉包詳解

在函式程式設計中經常用到閉包。閉包是什麼,它是怎麼產生的及用來解決什麼問題呢。給出字面的定義先:閉包是由函式及其相關的引用環境組合而成的實體(即:閉包=函式+引用環境)(想想Erlang的外層函式傳入一個引數a, 內層函式依舊傳入一個引數b, 內層函式使用a和b, 最後返回內層函式) 。這個從字面上很難理解,特別對於一直使用命令式語言進行程式設計的程式設計師們。本文將結合例項程式碼進行解釋。
函式是什麼
地球人都知道:函式只是一段可執行程式碼,編譯後就“固化”了,每個函式在記憶體中只有一份例項,得到函式的入口點便可以執行函數了。在函數語言程式設計語言中,函 數是一等公民(First class value:第一類物件,我們不需要像命令式語言中那樣藉助函式指標,委託操作函式),函式可以作為另一個函式的引數或返回值

,可以賦給一個變數。函式可 以巢狀定義,即在一個函式內部可以定義另一個函式,有了巢狀函式這種結構,便會產生閉包問題。如:

>>> def ExFunc(n):
     sum=n
     def InsFunc():
             return sum+1
     return InsFunc
     
>>> myFunc=ExFunc(10)

>>> myFunc()

>>> myAnotherFunc=ExFunc(20)

>>> myAnotherFunc()

>>
> myFunc() >>> myAnotherFunc()

在這段程式中,函式InsFunc是函式ExFunc的內嵌函式,並且是ExFunc函式的返回值。我們注意到一個問題:內嵌函式InsFunc中 引用到外層函式中的區域性變數sum,IronPython會這麼處理這個問題呢?先讓我們來看看這段程式碼的執行結果。當我們呼叫分別由不同的引數呼叫 ExFunc函式得到的函式時(myFunc(),myAnotherFunc()),得到的結果是隔離的,也就是說每次呼叫ExFunc函式後都將生成並儲存一個新的區域性變數sum。其實這裡ExFunc函式返回的就是閉包。

引用環境
按照命令式語言的規則,ExFunc函式只是返回了內嵌函式InsFunc的地址,在執行InsFunc函式時將會由於在其作用域內找不到sum變數而出 錯。而在函式式語言中,當內嵌函式體內引用到體外的變數時,將會把定義時涉及到的引用環境和函式體打包成一個整體(閉包)返回

。現在給出引用環境的定義就 容易理解了:引用環境是指在程式執行中的某個點所有處於活躍狀態的約束(一個變數的名字和其所代表的物件之間的聯絡)所組成的集合。閉包的使用和正常的函 數呼叫沒有區別。

由於閉包把函式和執行時的引用環境打包成為一個新的整體,所以就解決了函式程式設計中的巢狀所引發的問題。如上述程式碼段中,當每次呼叫ExFunc函式 時都將返回一個新的閉包例項,這些例項之間是隔離的,分別包含呼叫時不同的引用環境現場。不同於函式,閉包在執行時可以有多個例項,不同的引用環境和相同 的函式組合可以產生不同的例項。

一,定義
python中的閉包從表現形式上定義(解釋)為:如果在一個內部函式裡,對在外部作用域(但不是在全域性作用域)的變數進行引用,那麼內部函式就被認為是閉包(closure).這個定義是相對直白的,好理解的,不像其他定義那樣學究味道十足(那些學究味道重的解釋,在對一個名詞的解釋過程中又充滿了一堆讓人抓狂的其他陌生名詞,不適合初學者)。下面舉一個簡單的例子來說明。

>>>def addx(x):  
>>>    def adder(y): return x + y  
>>>    return adder  
>>> c =  addx(8)  
>>> type(c)  
<type 'function'>  
>>> c.__name__  
'adder'  
>>> c(10)

結合這段簡單的程式碼和定義來說明閉包:
如果在一個內部函式裡:adder(y)就是這個內部函式,
對在外部作用域(但不是在全域性作用域)的變數進行引用:x就是被引用的變數,x在外部作用域addx裡面,但不在全域性作用域裡,
則這個內部函式adder就是一個閉包。
再稍微講究一點的解釋是,閉包=函式塊+定義函式時的環境,adder就是函式塊,x就是環境,當然這個環境可以有很多,不止一個簡單的x。
二,使用閉包注意事項
1,閉包中是不能修改外部作用域的區域性變數的

>>> def foo():  
...     m = 0  
...     def foo1():  
...         m = 1  
...         print m  
...  
...     print m  
...     foo1()  
...     print m  
...  
>>> foo()

從執行結果可以看出,雖然在閉包裡面也定義了一個變數m,但是其不會改變外部函式中的區域性變數m。

2,以下這段程式碼是在python中使用閉包時一段經典的錯誤程式碼

def foo():  
    a = 1  
    def bar():  
        a = a + 1  
        return a  
    return bar

這段程式的本意是要通過在每次呼叫閉包函式時都對變數a進行遞增的操作。但在實際使用時

>>> c = foo()  
>>> print c()  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
  File "<stdin>", line 4, in bar  
UnboundLocalError: local variable 'a' referenced before assignment

這是因為在執行程式碼 c = foo()時,python會匯入全部的閉包函式體bar()來分析其的區域性變數,python規則指定所有在賦值語句左面的變數都是區域性變數,則在閉包bar()中,變數a在賦值符號"="的左面,被python認為是bar()中的區域性變數 。再接下來執行print c()時,程式執行至a = a + 1時,因為先前已經把a歸為bar()中的區域性變數,所以python會在bar()中去找在賦值語句右面的a的值,結果找不到,就會報錯。解決的方法很簡單

def foo():  
    a = [1]  
    def bar():  
        a[0] = a[0] + 1  
        return a[0]  
    return bar

只要將a設定為一個容器就可以了。這樣使用起來多少有點不爽,所以在python3以後,在a = a + 1 之前,使用語句nonloacal a就可以了,該語句顯式的指定a不是閉包的區域性變數。

3,還有一個容易產生錯誤的事例也經常被人在介紹python閉包時提起,我一直都沒覺得這個錯誤和閉包有什麼太大的關係,但是它倒是的確是在python函數語言程式設計是容易犯的一個錯誤,我在這裡也不妨介紹一下。先看下面這段程式碼

for i in range(3):  
    print i

在程式裡面經常會出現這類的迴圈語句,Python的問題就在於,當迴圈結束以後,迴圈體中的臨時變數i不會銷燬,而是繼續存在於執行環境中。還有一個python的現象是,python的函式只有在執行時,才會去找函式體裡的變數的值。

flist = []  
for i in range(3):  
    def foo(x): print x + i  
    flist.append(foo)  
for f in flist:  
    f(2)

可能有些人認為這段程式碼的執行結果應該是2,3,4.但是實際的結果是4,4,4。這是因為當把函式加入flist列表裡時,python還沒有給i賦值,只有當執行時,再去找i的值是什麼,這時在第一個for迴圈結束以後,i的值是2,所以以上程式碼的執行結果是4,4,4.
解決方法也很簡單,改寫一下函式的定義就可以了。

for i in range(3):  
    def foo(x,y=i): print x + y  
    flist.append(foo) 

三,作用
說了這麼多,不免有人要問,那這個閉包在實際的開發中有什麼用呢?閉包主要是在函式式開發過程中使用。以下介紹兩種閉包主要的用途。

用途1,當閉包執行完後,仍然能夠保持住當前的執行環境。
比如說,如果你希望函式的每次執行結果,都是基於這個函式上次的執行結果。我以一個類似棋盤遊戲的例子來說明。假設棋盤大小為50*50,左上角為座標系原點(0,0),我需要一個函式,接收2個引數,分別為方向(direction),步長(step),該函式控制棋子的運動。棋子運動的新的座標除了依賴於方向和步長以外,當然還要根據原來所處的座標點,用閉包就可以保持住這個棋子原來所處的座標。

origin = [0, 0]  # 座標系統原點  
legal_x = [0, 50]  # x軸方向的合法座標  
legal_y = [0, 50]  # y軸方向的合法座標  
def create(pos=origin):  
    def player(direction,step):  
        # 這裡應該首先判斷引數direction,step的合法性,比如direction不能斜著走,step不能為負等  
        # 然後還要對新生成的x,y座標的合法性進行判斷處理,這裡主要是想介紹閉包,就不詳細寫了。  
        new_x = pos[0] + direction[0]*step  
        new_y = pos[1] + direction[1]*step  
        pos[0] = new_x  
        pos[1] = new_y  
        #注意!此處不能寫成 pos = [new_x, new_y],原因在上文有說過  
        return pos  
    return player  
 
player = create()  # 建立棋子player,起點為原點  
print player([1,0],10)  # 向x軸正方向移動10步  
print player([0,1],20)  # 向y軸正方向移動20步  
print player([-1,0],10)  # 向x軸負方向移動10步

輸出為

[10, 0]  
[10, 20]  
[0, 20]  

用途2,閉包可以根據外部作用域的區域性變數來得到不同的結果,這有點像一種類似配置功能的作用,我們可以修改外部的變數,閉包根據這個變數展現出不同的功能。比如有時我們需要對某些檔案的特殊行進行分析,先要提取出這些特殊行

def make_filter(keep):  
    def the_filter(file_name):  
        file = open(file_name)  
        lines = file.readlines()  
        file.close()  
        filter_doc = [i for i in lines if keep in i]  
        return filter_doc  
    return the_filter

如果我們需要取得檔案"result.txt"中含有"pass"關鍵字的行,則可以這樣使用例子程式

filter = make_filter("pass")  
filter_result = filter("result.txt")  

以上兩種使用場景,用面向物件也是可以很簡單的實現的,但是在用Python進行函數語言程式設計時,閉包對資料的持久化以及按配置產生不同的功能,是很有幫助的