Python 閉包小記

閉包就是可以讀取其餘函數內部變量的函數。例如在javascript中,只有函數內部的子函數才能讀取局部變量,因此閉包能夠理解成「定義在一個函數內部的函數「。在本質上,閉包是將函數內部和函數外部鏈接起來的橋樑。javascript

上面這段話引自百度百科,濤濤以爲對於閉包的解釋通俗易懂,言簡意賅。java

 

對於 Python ,濤濤目前研究不是很深,尚在學習當中,因此如下對 Python 閉包的解釋案例多引自其餘大神,本身也就搬搬磚,特此寫下這篇,鞏固一下本身的知識儲備。python

 

首先列出閉包的必要條件:閉包

一、閉包函數必須返回一個函數對象app

二、閉包函數返回的那個函數必須引用外部變量(通常不能是全局變量),而返回的那個函數內部不必定要return函數

 

如下是幾個經典的閉包案例:學習

 1 # ENV>>> Python 3.6
 2     # NO.1
 3     def line_conf(a, b):  4         def line(x):  5             return a * x + b  6         return line  7     
 8     # NO.2
 9     def line_conf(): 10         a = 1
11         b = 2
12  
13         def line(x): 14             print(a * x + b) 15         return line 16  
17     # NO.3
18     def _line_(a,b): 19         def line_c(c): 20             def line(x): 21                 return a*(x**2)+b*x+c 22             return line 23         return line_c

1、函數中的做用域this

Python中函數的做用域由def關鍵字界定,函數內的代碼訪問變量的方式是從其所在層級由內向外的,如「熱身」中的第一段代碼:spa

1   def line_conf(a, b): 2         def line(x): 3             return a * x + b 4         return line

 

嵌套函數line中的代碼訪問了a和b變量,line自己函數體內並不存在這兩個變量,因此會逐級向外查找,往上走一層就找到了來自主函數line_conf傳遞的a, b。若往外直至全局做用域都查找不到的話代碼會拋異常。debug

 

注意:無論主函數line_conf下面嵌套了多少個函數,這些函數都在其做用域內,均可以在line_conf做用域內被調用。

 

思考上面這段代碼實現了什麼功能?

1     #定義兩條直線
2     line_A = line_conf(2, 1) #y=2x+b
3     line_B = line_conf(3, 2) #y=3x+2
4     
5     #打印x對應y的值
6     print(line_A(1)) #3
7     print(line_B(1)) #5

 

是否感受「哎喲,有點意思~」,更有意思的在後面呢。

 

如今不使用閉包,看看須要多少行代碼實現這個功能:

1     def line_A(x): 2         return 2*x+1
3     def line_B(x): 4         return 3*x+2
5     
6     print(line_A(1)) #3
7     print(line_B(1)) #5

不包括print語句的代碼是4行,閉包寫法是6行,看起來有點不對勁啊?怎麼閉包實現須要的代碼量還多呢?別急,我如今有個需求:

 

 再定義100條直線!

 

那麼如今誰的代碼量更少呢?很明顯這個是能夠簡單計算出來的,採用閉包的方式添加一條直線只須要加一行代碼,而普通作法須要添兩行代碼,定義100條直線兩種作法的代碼量差爲:100+6 -(100*2+4) = -98。須要注意的是,實際環境中定義的單個函數的代碼量多達幾十上百行,這時候閉包的做用就顯現出來了,沒錯,大大提升了代碼的可複用性!

 

注意:閉包函數引用的外部變量不必定就是其父函數的參數,也能夠是父函數做用域內的任意變量,如「熱身」中的第二段代碼:

1 def line_conf(): 2         a = 1
3         b = 2
4  
5         def line(x): 6             print(a * x + b) 7         return line

2、如何顯式地查看「閉包」

接上面的代碼塊:

1     L = line_conf() 2     print(line_conf().__closure__) #(<cell at 0x05BE3530: int object at 0x1DA2D1D0>,
3     # <cell at 0x05C4DDD0: int object at 0x1DA2D1E0>)
4     for i in line_conf().__closure__: #打印引用的外部變量值
5         print(i.cell_contents) #1 ; #2

__closure__屬性返回的是一個元組對象,包含了閉包引用的外部變量。

 

 若主函數內的閉包不引用外部變量,就不存在閉包,主函數的_closure__屬性永遠爲None:

 1     def line_conf():  2         a = 1
 3         b = 2
 4         def line(x):  5             print(x+1)  #<<<------
 6         return line  7     L = line_conf()  8     print(line_conf().__closure__) # None
 9     for i in line_conf().__closure__: #拋出異常
10         print(i.cell_contents)


若主函數沒有return子函數,就不存在閉包,主函數不存在_closure__屬性:

1 def line_conf(): 2         a = 1
3         b = 2
4         def line(x): 5             print(a*x+b) 6         return a+b #<<<------
7     L = line_conf() 8     print(line_conf().__closure__) # 拋出異常 

3、爲什麼叫閉包?

 

先看代碼:

1     def line_conf(a): 2         b=1
3         def line(x): 4             return a * x + b 5         return line 6  
7     line_A = line_conf(2) 8     b=20
9     print(line_A(1))  # 3

如你所見,line_A對象做爲line_conf返回的閉包對象,它引用了line_conf下的變量b=1,在print時,全局做用域下定義了新的b變量指向20,最終結果仍然引用的line_conf內的b。這是由於,閉包做爲對象被返回時,它的引用變量就已經肯定(已經保存在它的__closure__屬性中),不會再被修改。

 

 是的,閉包在被返回時,它的全部變量就已經固定,造成了一個封閉的對象,這個對象包含了其引用的全部外部、內部變量和表達式。固然,閉包的參數例外。

 

4、閉包能夠保存運行環境

 

思考下面的代碼會輸出什麼?

1     _list = [] 2     for i in range(3): 3         def func(a): 4             return i+a 5  _list.append(func) 6     for f in _list: 7         print(f(1))


1 , 2,  3嗎?若是不是又該是什麼呢?    結果是3, 3, 3 。

 

由於,在Python中,循環體內定義的函數是沒法保存循環執行過程當中的不停變化的外部變量的,即普通函數沒法保存運行環境!想要讓上面的代碼輸出1, 2, 3並不難,「術業有專攻」,這種事情該讓閉包來:

 1     _list = []  2     for i in range(3):  3         def func(i):  4             def f_closure(a):  # <<<---
 5                 return i + a  6             return f_closure  7         _list.append(func(i))  # <<<---
 8         
 9     for f in _list: 10         print(f(1))


5、閉包的實際應用

 

如今你已經逐漸領悟「閉包」了,趁熱打鐵,再來一個小例子:

 

 1     def who(name):  2         def do(what):  3             print(name, 'say:', what)  4  
 5         return do  6  
 7     lucy = who('lucy')  8     john = who('john')  9  
10     lucy('i want drink!') 11     lucy('i want eat !') 12     lucy('i want play !') 13     
14     john('i want play basketball') 15     john('i want to sleep with U,do U?') 16  
17     lucy("you just like a fool, but i got you!")

看到這裏,你也能夠試着本身寫出一個簡單的閉包函數。

 

OK,如今來看一個真正在實際環境中會用到的案例:

 

 一、【閉包實現快速給不一樣項目記錄日誌】

 1     import logging  2     def log_header(logger_name):  3         logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(name)s] %(levelname)s %(message)s',  4                             datefmt='%Y-%m-%d %H:%M:%S')  5         logger = logging.getLogger(logger_name)  6  
 7         def _logging(something,level):  8             if level == 'debug':  9  logger.debug(something) 10             elif level == 'warning': 11  logger.warning(something) 12             elif level == 'error': 13  logger.error(something) 14             else: 15                 raise Exception("I dont know what you want to do?" ) 16         return _logging 17  
18     project_1_logging = log_header('project_1') 19  
20     project_2_logging = log_header('project_2') 21  
22     def project_1(): 23  
24         #do something
25         project_1_logging('this is a debug info','debug') 26         #do something
27         project_1_logging('this is a warning info','warning') 28         # do something
29         project_1_logging('this is a error info','error') 30  
31     def project_2(): 32  
33         # do something
34         project_2_logging('this is a debug info','debug') 35         # do something
36         project_2_logging('this is a warning info','warning') 37         # do something
38         project_2_logging('this is a critical info','error') 39  
40  project_1() 41     project_2()

 

 

1 #輸出
2 2018-05-26 22:56:23 [project_1] DEBUG  this is a debug info 3 2018-05-26 22:56:23 [project_1] WARNING  this is a warning info 4 2018-05-26 22:56:23 [project_1] ERROR  this is a error info 5 2018-05-26 22:56:23 [project_2] DEBUG  this is a debug info 6 2018-05-26 22:56:23 [project_2] WARNING  this is a warning info 7 2018-05-26 22:56:23 [project_2] ERROR  this is a critical info

這段代碼實現了給不一樣項目logging的功能,只需在你想要logging的位置添加一行代碼便可。

 

擴展: python中的裝飾器特性就是利用閉包實現的,只不過用了@做爲語法糖,使寫法更簡潔。若是掌握了閉包,接下來就去看一下裝飾器,也會很快掌握的。

python閉包到底有什麼做用

 

一、global關鍵字的做用

若是在函數中須要修改全局變量,則須要使用該關鍵字,具體參見下面例子。

1 variable=100
2 def function(): 3     print(variable) #在函數內不對全局變量修改,直接訪問是沒問題的,不會報錯
4 function() #輸出100

 

1 variable=100
2 def function(): 3     result = variable + 111
4     print(result) #在函數內不對全局變量修改,直接訪問是沒問題的,不會報錯
5 function() #輸出100

 

1 variable = 100
2 def function(): 3     variable += 111
4     print(variable)  # 顯示local variable 'variable' referenced before assignment。
5                      # 即在函數局部做用域中直接改變全局變量的值會報錯
6 function()

 

1 variable = 100
2 def function(): 3     variable = 1000  # 此時修改variable變量的值不會報錯,由於已經在函數局部做用域內從新定義variable,會覆蓋全局variable。
4     variable += 111
5     print(variable) 6 function()  # 輸出1111
7 print(variable)  # 輸出100,雖然函數內部從新覆蓋了variable,可是全局variable並未變,依然仍是100

 

那若是再也不函數內部從新爲全局變量賦值,又想改變全局變量的值,應該怎麼作呢?這就要使用global關鍵字了,以下。

 

1 variable = 100
2 def function(): 3     global variable  # 使用global關鍵字,代表variable是全局的,此時就能夠了直接在函數局部做用域內改變variable的值了
4     variable += 111
5     print(variable)  # 輸出211
6 function() 7 print(variable)  # 輸出211,這和上面的不同了,發現全局變量variable自己也改變了

 

總結:global的做用就是在「函數局部做用域」內聲明表示一個全局變量,從而能夠在函數內部修改全局變量的值(不然只能訪問不能修改),並且函數內部改變的全局變量的值也會改變。

 

二、函數局部做用域

 

函數的局部做用域是不可以保存信息的,即在函數內部聲明變量在函數調用結束以後函數裏面保存的信息就被銷燬了,包括函數的參數,以下。

1 def fun(step): 2     num = 1
3     num += step 4     print(num) 5 i = 1
6 while (i < 5): 7     fun(3)  # 連續調用函數4次,每次輸出的值都是4,即3+1,這說明每次調用fun函數以後,函數內部定義局部變量num就被銷燬了,
8 # 沒有保存下來,說明函數的局部做用域被銷燬了。那若是要保存函數的局部變量,怎麼辦呢?引入「閉包」。
9 i += 1

 

三、閉包——裝飾器的本質也是閉包

 

「閉包」的本質就是函數的嵌套定義,即在函數內部再定義函數,以下所示。

 

「閉包」有兩種不一樣的方式,第一種是在函數內部就「直接調用了」;第二種是「返回一個函數名稱」。

 

(1)第一種形式——直接調用

1 def Maker(name): 2     num=100
3     def func1(weight,height,age): 4         weight+=1
5         height+=1
6         age+=1
7         print(name,weight,height,age) 8 func1(100,200,300) #在內部就直接調用「內部函數」
9 Maker('feifei') #調用外部函數,輸出 feifei 101 201 301

(2)第二種形式——返回函數名稱

 1 def Maker(name):  2     num=100
 3     def func1(weight,height,age):  4         weight+=1
 5         height+=1
 6         age+=1
 7         print(name,weight,height,age)  8     return func1 #此處不直接調用,而是返回函數名稱(Python中一切皆對象)
 9 maker=Maker('feifei') #調用包裝器
10 maker(100,200,300) #調用內部函數


(3)「閉包」的做用——保存函數的狀態信息,使函數的局部變量信息依然能夠保存下來,以下。

 

 1 def Maker(step): #包裝器
 2     num=1
 3     def fun1(): #內部函數
 4         nonlocal num #nonlocal關鍵字的做用和前面的local是同樣的,若是不使用該關鍵字,則不能再內部函數改變「外部變量」的值
 5         num=num+step #改變外部變量的值(若是隻是訪問外部變量,則不須要適用nonlocal)
 6         print(num)  7     return fun1  8 #=====================================#
 9 j=1
10 func2=Maker(3) #調用外部包裝器
11 while(j<5): 12 func2() #調用內部函數4次 輸出的結果是 四、七、十、13
13 j+=1

 

從上面的例子能夠看出,外部裝飾器函數的局部變量num=一、以及調用裝飾器Maker(3)時候傳入的參數step=3都被記憶了下來,因此纔有1+3=四、4+3=七、7+3=十、10+3=13.

 

從這裏能夠看出,Maker函數雖然調用了,可是它的局部變量信息卻被保存了下來,這就是「閉包」的最大的做用——保存局部信息不被銷燬。

 

四、nonlocal關鍵字的做用

 

該關鍵字的做用和local的做用相似,就是讓「內部函數」能夠修改「外部函數(裝飾器)」的局部變量值。詳細信息這裏不作討論。

 

以上就是 Python 閉包學習記錄的內容,在此特別感謝 CSDN 的 chaseSpace-L 和 LoveMIss-Y 兩位大神的文章和解釋!

相關文章
相關標籤/搜索