理解Python閉包概念

閉包並不僅是一個python中的概念,在函數式編程語言中應用較爲普遍。理解python中的閉包一方面是可以正確的使用閉包,另外一方面能夠好好體會和思考閉包的設計思想。html

1.概念介紹

首先看一下維基上對閉包的解釋:python

在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即便已經離開了創造它的環境也不例外。因此,有另外一種說法認爲閉包是由函數和與其相關的引用環境組合而成的實體。閉包在運行時能夠有多個實例,不一樣的引用環境和相同的函數組合能夠產生不一樣的實例。編程

簡單來講就是一個函數定義中引用了函數外定義的變量,而且該函數能夠在其定義環境外被執行。這樣的一個函數咱們稱之爲閉包。實際上閉包能夠看作一種更加廣義的函數概念。由於其已經再也不是傳統意義上定義的函數。數組

根據咱們對編程語言中函數的理解,大概印象中的函數是這樣的:閉包

程序被加載到內存執行時,函數定義的代碼被存放在代碼段中。函數被調用時,會在棧上建立其執行環境,也就是初始化其中定義的變量和外部傳入的形參以便函數進行下一步的執行操做。當函數執行完成並返回函數結果後,函數棧幀便會被銷燬掉。函數中的臨時變量以及存儲的中間計算結果都不會保留。下次調用時惟一發生變化的就是函數傳入的形參可能會不同。函數棧幀會從新初始化函數的執行環境。app

C++中有static關鍵字,函數中的static關鍵字定義的變量獨立於函數以外,並且會保留函數中值的變化。函數中使用的全局變量也有相似的性質。編程語言

可是閉包中引用的函數定義以外的變量是否能夠這麼理解呢?可是若是函數中引用的變量既不是全局的,也不是靜態的(python中沒有這個概念)。應該怎麼正確的理解呢?函數式編程

建議先參考一下個人另外一篇博文(Python  UnboundLocalError和NameError錯誤根源解析 ),瞭解一下變量可見性和綁定相關的概念很是有必要。函數

2.閉包初探

爲了說明閉包中引用的變量的性質,能夠看一下下面的這個例子:學習

 1 def outer_func():
 2     loc_list = []
 3     def inner_func(name):
 4         loc_list.append(len(loc_list) + 1)
 5         print '%s loc_list = %s' %(name, loc_list)
 6     return inner_func
 7 
 8 clo_func_0 = outer_func()
 9 clo_func_0('clo_func_0')
10 clo_func_0('clo_func_0')
11 clo_func_0('clo_func_0')
12 clo_func_1 = outer_func()
13 clo_func_1('clo_func_1')
14 clo_func_0('clo_func_0')
15 clo_func_1('clo_func_1')

程序的運行結果:

clo_func_0 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_0 loc_list = [1, 2, 3]
clo_func_1 loc_list = [1]
clo_func_0 loc_list = [1, 2, 3, 4]
clo_func_1 loc_list = [1, 2]

從上面這個簡單的例子應該對閉包有一個直觀的理解了。運行的結果也說明了閉包函數中引用的父函數中local variable既不具備C++中的全局變量的性質也沒有static變量的行爲。

在python中咱們稱上面的這個loc_list爲閉包函數inner_func的一個自由變量(free variable)。

If a name is bound in a block, it is a local variable of that block. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is used in a code block but not defined there, it is a free variable.

在這個例子中咱們至少能夠對閉包中引用的自由變量有以下的認識:

  • 閉包中的引用的自由變量只和具體的閉包有關聯,閉包的每一個實例引用的自由變量互不干擾。
  • 一個閉包實例對其自由變量的修改會被傳遞到下一次該閉包實例的調用。

因爲這個概念理解起來並非那麼的直觀,所以使用的時候很容易掉進陷阱。

3.閉包陷阱

下面先來看一個例子:

 1 def my_func(*args):
 2     fs = []
 3     for i in xrange(3):
 4         def func():
 5             return i * i
 6         fs.append(func)
 7     return fs
 8 
 9 fs1, fs2, fs3 = my_func()
10 print fs1()
11 print fs2()
12 print fs3()

 上面這段代碼可謂是典型的錯誤使用閉包的例子。程序的結果並非咱們想象的結果0,1,4。實際結果所有是4。

這個例子中,my_func返回的並非一個閉包函數,而是一個包含三個閉包函數的一個list。這個例子中比較特殊的地方就是返回的全部閉包函數均引用父函數中定義的同一個自由變量。

但這裏的問題是爲何for循環中的變量變化會影響到全部的閉包函數?尤爲是咱們上面剛剛介紹的例子中明明說明了同一閉包的不一樣實例中引用的自由變量互相沒有影響的。並且這個觀點也絕對的正確。

那麼問題到底出在哪裏?應該怎樣正確的分析這個錯誤的根源。

其實問題的關鍵就在於在返回閉包列表fs以前for循環的變量的值已經發生改變了,並且這個改變會影響到全部引用它的內部定義的函數。由於在函數my_func返回前其內部定義的函數並非閉包函數,只是一個內部定義的函數。

固然這個內部函數引用的父函數中定義的變量也不是自由變量,而只是當前block中的一個local variable。

1 def my_func(*args):
2     fs = []
3     j = 0
4     for i in xrange(3):
5         def func():
6             return j * j
7         fs.append(func)
8     j = 2
9     return fs

 上面的這段代碼邏輯上與以前的例子是等價的。這裏或許更好理解一點,由於在內部定義的函數func實際執行前,對局部變量j的任何改變均會影響到函數func的運行結果。

函數my_func一旦返回,那麼內部定義的函數func即是一個閉包,其中引用的變量j成爲一個只和具體閉包相關的自由變量。後面會分析,這個自由變量存放在Cell對象中。

使用lambda表達式重寫這個例子:

1 def my_func(*args):
2     fs = []
3     for i in xrange(3):
4         func = lambda : i * i
5         fs.append(func)
6     return fs

通過上面的分析,咱們得出下面一個重要的經驗:返回閉包中不要引用任何循環變量,或者後續會發生變化的變量。

這條規則本質上是在返回閉包前,閉包中引用的父函數中定義變量的值可能會發生不是咱們指望的變化。

正確的寫法

1 def my_func(*args):
2     fs = []
3     for i in xrange(3):
4         def func(_i = i):
5             return _i * _i
6         fs.append(func)
7     return fs

或者:

1 def my_func(*args):
2     fs = []
3     for i in xrange(3):
4         func = lambda _i = i : _i * _i
5         fs.append(func)
6     return fs

正確的作法即是將父函數的local variable賦值給函數的形參。函數定義時,對形參的不一樣賦值會保留在當前函數定義中,不會對其餘函數有影響。

另外注意一點,若是返回的函數中沒有引用父函數中定義的local variable,那麼返回的函數不是閉包函數。

4.閉包的應用

自由變元能夠記錄閉包函數被調用的信息,以及閉包函數的一些計算結果中間值。並且被自由變量記錄的值,在下次調用閉包函數時依舊有效。

根據閉包函數中引用的自由變量的一些特性,閉包的應用場景仍是比較普遍的。後面會有文章介紹其應用場景之一——單例模式,限於篇幅,此處以裝飾器爲例介紹一下閉包的應用。

若是咱們想對一個函數或者類進行修改重定義,最簡單的方法就是直接修改其定義。可是這種作法的缺點也是顯而易見的:

  • 可能看不到函數或者類的定義
  • 會破壞原來的定義,致使原來對類的引用不兼容
  • 若是多人想在原來的基礎上定製本身函數,很容易衝突

 使用閉包能夠相對簡單的解決上面的問題,下面看一個例子:

 1 def func_dec(func):
 2     def wrapper(*args):
 3         if len(args) == 2:
 4             func(*args)
 5         else:
 6             print 'Error! Arguments = %s'%list(args)
 7     return wrapper
 8 
 9 @func_dec
10 def add_sum(*args):
11     print sum(args)
12 
13 # add_sum = func_dec(add_sum)
14 args = range(1,3)
15 add_sum(*args)

 對於上面的這個例子,並無破壞add_sum函數的定義,只不過是對其進行了一層簡單的封裝。若是看不到函數的定義,也能夠對函數對象進行封裝,達到相同的效果(即上面註釋掉的13行),並且裝飾器是能夠疊加使用的。

4.1 潛在的問題

但閉包的缺點也是很明顯的,那就是通過裝飾器裝飾的函數或者類再也不是原來的函數或者類了。這也是使用裝飾器改變函數或者類的行爲與直接修改定義最根本的差異。

實際應用的時候必定要注意這一點,下面看一個使用裝飾器致使的一個很隱蔽的問題。

 1 def counter(cls):
 2     obj_list = []
 3     def wrapper(*args, **kwargs):
 4         new_obj = cls(*args, **kwargs)
 5         obj_list.append(new_obj)
 6         print "class:%s'object number is %d" % (cls.__name__, len(obj_list))
 7         return new_obj
 8     return wrapper
 9 
10 @counter
11 class my_cls(object):
12     STATIC_MEM = 'This is a static member of my_cls'
13     def __init__(self, *args, **kwargs):
14         print self, args, kwargs
15         print my_cls.STATIC_MEM

  這個例子中咱們嘗試使用裝飾器來統計一個類建立的對象數量。當咱們建立my_cls的對象時,會發現something is wrong!

Traceback (most recent call last):
  File "G:\Cnblogs\Alpha Panda\Main.py", line 360, in <module>
    my_cls(1,2, key = 'shijun')
  File "G:\Cnblogs\Alpha Panda\Main.py", line 347, in wrapper
    new_obj = cls(*args, **kwargs)
  File "G:\Cnblogs\Alpha Panda\Main.py", line 358, in __init__
    print my_cls.STATIC_MEM
AttributeError: 'function' object has no attribute 'STATIC_MEM'

 若是對裝飾器不是特別的瞭解,可能會對這個錯誤感到詫異。通過裝飾器修飾後,咱們定義的類my_cls已經成爲一個函數。

my_cls.__name__ == 'wrapper' and type(my_cls) is types.FunctionType

 my_cls被裝飾器counter修飾,等價於 my_cls = counter(my_cls)

顯然在上面的例子中,my_cls.STATIC_MEM是錯誤的,正確的用法是self.STATIC_MEM。

對象中找不到屬性的話,會到類空間中尋找,所以被裝飾器修飾的類的靜態屬性是能夠經過其對象進行訪問的。雖然my_cls已經不是類,可是其調用返回的值倒是被裝飾以前的類的對象。

該問題一樣適用於staticmethod。那麼有沒有方法獲得原來的類呢?固然能夠,my_cls().__class__即是被裝飾以前的類的定義。

那有沒有什麼方法能讓咱們還能經過my_cls來訪問類的靜態屬性,答案是確定的。

1 def counter(cls):
2     obj_list = []
3     @functools.wraps(cls)
4     def wrapper(*args, **kwargs):
5         ... ...
6     return wrapper

改寫裝飾器counter的定義,主要是對wrapper使用functools進行了一次包裹更新,使通過裝飾的my_cls看起來更像裝飾以前的類或者函數。該過程的主要原理就是將被裝飾類或者函數的部分屬性直接賦值到裝飾以後的對象。如WRAPPER_ASSIGNMENTS(__name__, __module__ and __doc__, )和WRAPPER_UPDATES(__dict__)等。可是該過程不會改變wrapper是函數這樣一個事實。

my_cls.__name__ == 'my_cls' and type(my_cls) is types.FunctionType

5.閉包的實現

本着會用加理解的原則,能夠從應用層的角度來稍微深刻的理解一下閉包的實現。畢竟要先會用python麼,若是一切都從源碼中學習,那成本的確有點高。

 1 def outer_func():
 2     loc_var = "local variable"
 3     def inner_func():
 4         return loc_var
 5     return inner_func
 6 
 7 import dis
 8 dis.dis(outer_func)
 9 clo_func = outer_func()
10 print clo_func()
11 dis.dis(clo_func)

 爲了更加清楚理解上述過程,咱們先嚐試給出outer_func.func_code中的部分屬性:

  • outer_func.func_code.co_consts: (None, 'local variable', <code object inner_func at 025F7770, file "G:\Cnblogs\Alpha Panda\Main.py", line 207>)
  • outer_func.func_code.co_cellvars:('loc_var',)
  • outer_func.func_code.co_varnames:('inner_func',)

嘗試反彙編上面這個簡單清晰的閉包例子,獲得下面的結果:

2            0 LOAD_CONST               1 ('local variable')   # 將outer_func.func_code.co_consts[1]放到棧頂
             3 STORE_DEREF              0 (loc_var)        # 將棧頂元素存放到cell對象的slot 0 

3            6 LOAD_CLOSURE             0 (loc_var)        # 將outer_func.func_code.co_cellvars[0]對象的索引放到棧頂
             9 BUILD_TUPLE              1              # 將棧頂1個元素取出,建立元組並將元組壓入棧中
             12 LOAD_CONST              2 (<code object inner_func at 02597770, file "G:\Cnblogs\Alpha Panda\Main.py", line 207>) # 將outer_func.func_code.co_consts[2]放到棧頂
             15 MAKE_CLOSURE            0              # 建立閉包,此時棧頂是閉包函數代碼段的入口,棧頂下面則是函數的free variables,也就是本例中的'local variable ',將閉包壓入棧頂
             18 STORE_FAST              0 (inner_func)       # 將棧頂存放入outer_func.func_code.co_varnames[0]

5            21 LOAD_FAST               0 (inner_func)       # 將outer_func.func_code.co_varnames[0]的引用放入棧頂
             24 RETURN_VALUE                       # Returns with TOS to the caller of the function.
local variable
4            0 LOAD_DEREF               0 (loc_var)         # 將cell對象中的slot 0對象的引用壓入棧頂
             3 RETURN_VALUE                          # Returns with TOS to the caller of the function 

這個結果中,咱們反彙編了外層函數及其返回的閉包函數(爲了便於查看,修改了部分行號)。從對上面兩個函數的反彙編的註釋能夠大體瞭解閉包實現的步驟。

python閉包中引用的自由變量實際存放在一個Cell對象中,當自由變元被閉包引用時,便將Cell中存放的自由變量的引用放入棧頂。

本例中Cell對象及其存放的自由變量分別爲:

clo_func.func_closure[0]    #Cell Object
clo_func.func_closure[0].cell_contents == 'local variable'    # Free Variable

閉包實現的一個關鍵的地方是Cell Object,下面是官方給出的解釋:

「Cell」 objects are used to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contains a reference to the cells from outer scopes which also use that variable. When the value is accessed, the value contained in the cell is used instead of the cell object itself. This de-referencing of the cell object requires support from the generated byte-code; these are not automatically de-referenced when accessed. Cell objects are not likely to be useful elsewhere.

好了,限於篇幅就先介紹到這裏。重要的是理解的基礎上靈活的應用解決實際的問題並避免陷阱,但願本文能讓你對閉包有一個不同的認識。

相關文章
相關標籤/搜索