1.定義
閉包是函數式編程的一個重要的語法結構,函數式編程是一種編程范式 (而面向過程編程和面向對象編程也都是編程范式)。在面向過程編程中,我們見到過函數(function);在面向對象編程中,我們見過對象(object)。函數和對象的根本目的是以某種邏輯方式組織代碼,並提高代碼的可重復使用性(reusability)。閉包也是一種組織代碼的結構,它同樣提高了代碼的可重復使用性。
不同編程語言實現閉包的方式是不同的,python中閉包從表現形式上看,如果在一個內部函數里,對在外部作用域(但不是在全局作用域)的變量進行引用,那么內部函數就被認為是閉包(closure)。
舉個例子:
1 def outer(x): 2 def inner(y): 3 return x + y 4 return inner
結合這段簡單的代碼和定義來說明閉包:
inner(y)就是這個內部函數,對在外部作用域(但不是在全局作用域)的變量進行引用:x就是被引用的變量,x在外部作用域outer里面,但不在全局作用域里,則這個內部函數inner就是一個閉包。
再稍微講究一點的解釋是,閉包=函數塊+定義函數時的環境,inner就是函數塊,x就是環境,當然這個環境可以有很多,不止一個簡單的x。
在函數outer中定義了一個inner函數,inner函數訪問外部函數outer的(參數)變量,並且把inner函數作為返回值返回給outer函數。
1 a = outer(2) 2 print('function:',a) 3 print('result:',a(3))
上面的代碼中a就是一個函數,代碼的執行結果為:

從結果我們不難看出,a是函數inner而不是outer,這個有點繞,但是並不難理解,因為return回來的是inner函數。
1 print('a.func_name',a.func_name)
輸出結果為:
調用函數a,得到的結果是傳入參數的值相加。
上面的和這句是一樣的:print('result:',outer(2)(3))
2.使用閉包注意的地方
2.1閉包無法修改外部函數的局部變量

如果innerFunc可以修改x的值的話,x的值前后會發生變化,但結果是:

在innerFunc中x的值發生了改變,但是在outerFunc中x的值並未發生變化。
再來看一個例子
2.2閉包無法直接訪問外部函數的局部變量
1 def outer(): 2 x = 5 3 def inner(): #上面一行的x相對inner函數來說是函數外的局部變量(非全局變量) 4 x *= x 5 return x 6 return inner 7 8 outer()()
運行會報錯: 
解決的方法:
1.在python3之前沒有直接的解決方法,只能間接地通過容器類型來解決,因為容器類型不是存放在棧空間的,inner函數可以訪問到。
1 def outer(): 2 x = [5] 3 def inner(): 4 x[0] *= x[0] 5 return x[0] 6 return inner 7 8 print(outer()()) #25
2.python3通過nonlocal關鍵字來解決,該語句顯式的指定a不是閉包的局部變量。
1 def outer(): 2 x = 5 3 def inner(): 4 nonlocal x #把x聲明為非局部變量 5 x *= x 6 return x 7 return inner 8 9 print(outer()())
2.3python循環中不包含域的概念
還有一個容易產生錯誤的事例也經常被人在介紹python閉包時提起,我一直都沒覺得這個錯誤和閉包有什么太大的關系,但是它倒是的確是在python函數式編程是容易犯的一個錯誤,我在這里也不妨介紹一下。先看下面這段代碼
1 for i in range(3): 2 print i
在程序里面經常會出現這類的循環語句,Python的問題就在於,當循環結束以后,循環體中的臨時變量i不會銷毀,而是繼續存在於執行環境中。還有一個python的現象是,python的函數只有在執行時,才會去找函數體里的變量的值。
1 flist = [] 2 for i in range(3): 3 def foo(x): print x + i 4 flist.append(foo) 5 for f in flist: 6 f(2)
可能有些人認為這段代碼的執行結果應該是2,3,4.但是實際的結果是4,4,4。loop在python中是沒有域的概念的,flist在像列表中添加func的時候,並沒有保存i的值,而是當執行f(2)的時候才去取,這時候循環已經結束,i的值是2,所以結果都是4。
解決方法也很簡單,改寫一下函數的定義就可以了。
for i in range(3): def foo(x,y=i): print x + y flist.append(foo)
另外一個例子:
需要注意的問題是,返回的函數並沒有立刻執行,而是直到調用了f()才執行。我們來看一個例子:
1 function count() { 2 var arr = []; 3 for (var i=1; i<=3; i++) { 4 arr.push(function () { 5 return i * i; 6 }); 7 } 8 return arr; 9 } 10 11 var results = count(); 12 var f1 = results[0]; 13 var f2 = results[1]; 14 var f3 = results[2];
在上面的例子中,每次循環,都創建了一個新的函數,然后,把創建的3個函數都添加到一個Array中返回了。
你可能認為調用f1(),f2()和f3()結果應該是1,4,9,但實際結果是:
1 f1(); // 16 2 f2(); // 16 3 f3(); // 16
全部都是16!原因就在於返回的函數引用了變量i,但它並非立刻執行。等到3個函數都返回時,它們所引用的變量i已經變成了4,因此最終結果為16。
返回閉包時牢記的一點就是:返回函數不要引用任何循環變量,或者后續會發生變化的變量。
如果一定要引用循環變量怎么辦?方法是再創建一個函數,用該函數的參數綁定循環變量當前的值,無論該循環變量后續如何更改,已綁定到函數參數的值不變:
1 function count() { 2 var arr = []; 3 for (var i=1; i<=3; i++) { 4 arr.push((function (n) { 5 return function () { 6 return n * n; 7 } 8 })(i)); 9 } 10 return arr; 11 } 12 13 var results = count(); 14 var f1 = results[0]; 15 var f2 = results[1]; 16 var f3 = results[2]; 17 18 f1(); // 1 19 f2(); // 4 20 f3(); // 9
注意這里用了一個“創建一個匿名函數並立刻執行”的語法:
1 (function (x) { 2 return x * x; 3 })(3); // 9
理論上講,創建一個匿名函數並立刻執行可以這么寫:
1 function (x) { return x * x } (3);
但是由於JavaScript語法解析的問題,會報SyntaxError錯誤,因此需要用括號把整個函數定義括起來:
1 (function (x) { return x * x }) (3);
通常,一個立即執行的匿名函數可以把函數體拆開,一般這么寫:
1 (function (x) { 2 return x * x; 3 })(3);
3.閉包的作用
說了這么多,不免有人要問,那這個閉包在實際的開發中有什么用呢?閉包主要是在函數式開發過程中使用。以下介紹兩種閉包主要的用途。
用途1:當閉包執行完后,仍然能夠保持住當前的運行環境。
比如說,如果你希望函數的每次執行結果,都是基於這個函數上次的運行結果。我以一個類似棋盤游戲的例子來說明。假設棋盤大小為50*50,左上角為坐標系原點(0,0),我需要一個函數,接收2個參數,分別為方向(direction),步長(step),該函數控制棋子的運動。棋子運動的新的坐標除了依賴於方向和步長以外,當然還要根據原來所處的坐標點,用閉包就可以保持住這個棋子原來所處的坐標。
1 origin = [0, 0] 2 legal_x = [0, 50] 3 legal_y = [0, 50] 4 def create(pos=origin): 5 def player(direction,step): 6 # 這里應該首先判斷參數direction,step的合法性,比如direction不能斜着走,step不能為負等 7 # 然后還要對新生成的x,y坐標的合法性進行判斷處理,這里主要是想介紹閉包,就不詳細寫了。 8 new_x = pos[0] + direction[0]*step 9 new_y = pos[1] + direction[1]*step 10 pos[0] = new_x 11 pos[1] = new_y 12 #注意!此處不能寫成 pos = [new_x, new_y],因為參數變量不能被修改,而pos[]是容器類的解決方法 13 return pos 14 return player 15 16 player = create() # 創建棋子player,起點為原點 17 print player([1,0],10) # 向x軸正方向移動10步 18 print player([0,1],20) # 向y軸正方向移動20步 19 print player([-1,0],10) # 向x軸負方向移動10步
輸出為:
1 [10, 0] 2 [10, 20] 3 [0, 20]
用途2:閉包可以根據外部作用域的局部變量來得到不同的結果
這有點像一種類似配置功能的作用,我們可以修改外部的變量,閉包根據這個變量展現出不同的功能。比如有時我們需要對某些文件的特殊行進行分析,先要提取出這些特殊行。
1 def make_filter(keep): 2 def the_filter(file_name): 3 file = open(file_name) 4 lines = file.readlines() 5 file.close() 6 filter_doc = [i for i in lines if keep in i] 7 return filter_doc 8 return the_filter
如果我們需要取得文件”result.txt”中含有”pass”關鍵字的行,則可以這樣使用例子程序
1 filter = make_filter("pass") filter_result = filter("result.txt")
以上兩種使用場景,用面向對象也是可以很簡單的實現的,但是在用Python進行函數式編程時,閉包對數據的持久化以及按配置產生不同的功能,是很有幫助的。
