1. 程式人生 > >Python學習筆記: 閉包

Python學習筆記: 閉包

閉包的基本定義

在電腦科學中,閉包英語:Closure),又稱詞法閉包Lexical Closure)或函式閉包function closures),是引用了自由變數的函式。這個被引用的自由變數將和這個函式一同存在,即使已經離開了創造它的環境也不例外。所以,有另一種說法認為閉包是由函式和與其相關的引用環境組合而成的實體。閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。詳見 維基百科

 

Python 中的閉包

首先從例子引入 Python3 中的閉包

 1 #!/usr/bin/env python3
 2 #-*- coding:utf-8 -*-
3 #File Name:01_closure.py 4 #Created Time:2019-01-09 09:32:20 5 6 7 def line(k, b): 8 # 在函式內部再定義一個函式,並且這個函式用到了外部函式的變數, 9 # 這個內部函式的用到的相關變數即被稱為閉包 10 # 本例中,k,b 和 x 即為閉包 11 def foo(x): 12 return k*x + b 13 return foo # 返回內部函式的引用 14 15 16 if __name__ == "__main__":
17 18 print("***** y = x + 1 *****") 19 a = line(1,1) 20 print("x = 1 --> y = %d" % a(1)) 21 print("x = 2 --> y = %d" % a(2)) 22 print("x = 3 --> y = %d" % a(3)) 23 24 print("***** y = 4 * x + 1 *****") 25 b = line(4,1) 26 print("x = 1 --> y = %d" % b(1))
27 print("x = 2 --> y = %d" % b(2)) 28 print("x = 3 --> y = %d" % b(3))
例程

執行結果:

***** y = x + 1 *****
x = 1 --> y = 2
x = 2 --> y = 3
x = 3 --> y = 4
***** y = 4 * x + 1 *****
x = 1 --> y = 5
x = 2 --> y = 9
x = 3 --> y = 13

分析:

  1. 這個例子中,函式line與變數a,b構成閉包。在建立閉包的時候,通過line的引數 k, b 說明了這兩個變數的取值,這樣,就確定了函式的最終形式(y = x + 1和y = 4x + 1)。只需要變換引數a,b,就可以獲得不同的直線表達函式。由此,可以看出,閉包具有跟函式類似的提高程式碼可複用性的作用。
  2. 如果沒有閉包,每次新建立直線函式的時候需要同時指定 k,b,x。這樣,就需要更多的引數傳遞,也減少了程式碼的可移植性。

注意點:

  1. 由於閉包引用了外部函式的區域性變數,則外部函式的區域性變數沒有及時釋放,會消耗記憶體。
  2. 但是相較於類而言,其已經大大降低了記憶體佔用

為了進一步的對 Python3 中的閉包的呼叫流程進行說明,再看以下例程:

 1 #!/usr/bin/env python3
 2 #-*- coding:utf-8 -*-
 3 #File Name: 02_closure_test.py
 4 #Created Time:2019-01-09 09:32:20
 5  
 6 import sys
 7  
 8 def line(k, b):
 9     # 在函式內部再定義一個函式,並且這個函式用到了外部函式的變數,
10     # 這個內部函式的用到的相關變數即被稱為閉包
11     # 本例中,k,b 和 x 即為閉包
12     def foo(x):
13         return k*x + b
14     print("foo 函式的 id 為:\t\t%d" % id(foo))
15     return foo
16  
17  
18 if __name__ == "__main__":
19  
20     print("***** y = x + 1 *****")
21     print("line 的 id 為: \t\t\t%d" % id(line))
22     a = line(1,1) # 會列印 foo 的 id
23     print("a = line(1,1),a 的 id 為: \t%d" % id(a))
24     print("x = 1 --> y = %d" % a(1))
25     print("***** y = 3*x + 2 *****")
26     print("line 的 id 為: \t\t\t%d" % id(line))
27     b = line(3,2) # 會列印 foo 的 id
28     print("b = line(3,2),b 的 id 為: \t%d" % id(b))
29     print("x = 1 --> y = %d" % b(1))
閉包例程

執行結果: 

***** y = x + 1 *****
line 的 id 為:                  2452122878432
foo 函式的 id 為:               2452124629872
a = line(1,1),a 的 id 為:       2452124629872
x = 1 --> y = 2
***** y = 3*x + 2 *****
line 的 id 為:                  2452122878432
foo 函式的 id 為:               2452124630008
b = line(3,2),b 的 id 為:       2452124630008
x = 1 --> y = 5

 

 從結果中可看出,line 函式的 id 沒有發生改變,而不同閉包的 id 則各不相同。說明閉包在執行時可以有多個不同的例項

執行過程分析

Python 直譯器執行到 1 處時,會將 line 函式載入到記憶體中。

執行到 2 處時,會呼叫記憶體中的 line 函式,建立(圖中的 3 )相應的記憶體空間 8,通過 14 行中的 retrun 語句,使得 a 指向了記憶體 8 中的 foo 函式。 第 26 行執行的過程類似,只不過又開闢了一塊新的記憶體空間。

由於 line 函式中的 foo 被外部程式所引用,因此 8,9 記憶體空間並不會隨著 line 函式執行的結束而被釋放。從而使用相應的 foo 函式可以被重複呼叫。

 

Python3 閉包修改外部變數的值

可以藉助 noloacl 關鍵字,也可藉助列表,下面的例程演示 nolocal 關鍵字實現的方法:

 1 #!/usr/bin/env python3
 2 #-*- coding:utf-8 -*-
 3 #File Name: 03_closure_modify_local_var.py
 4 #Created Time:2019-01-09 09:32:20
 5  
 6 import sys
 7  
 8 def line(k, b):
 9     # 在函式內部再定義一個函式,並且這個函式用到了外部函式的變數,
10     # 這個內部函式的用到的相關變數即被稱為閉包
11     # 本例中,k,b 和 x 即為閉包
12     def foo(x):
13         nonlocal k
14         k += 1
15         print("***** k = %d" % k)
16         return k*x + b
17     return foo
18  
19  
20 if __name__ == "__main__":
21  
22     print("***** y = x + 1 *****")
23     a = line(1,1)
24     print("x = 1 --> y = %d" % a(1))
25     print("x = 2 --> y = %d" % a(2))
26     print("x = 3 --> y = %d" % a(3))
27  
28     print("***** y = 4*x + 1 *****")
29     b = line(4,1)
30     print("x = 1 --> y = %d" % b(1))
31     print("x = 2 --> y = %d" % b(2))
32     print("x = 3 --> y = %d" % b(3))
修改外部變數

 

執行結果:

***** y = x + 1 *****
***** k = 2
x = 1 --> y = 3
***** k = 3
x = 2 --> y = 7
***** k = 4
x = 3 --> y = 13
***** y = 4*x + 1 *****
***** k = 5
x = 1 --> y = 6
***** k = 6
x = 2 --> y = 13
***** k = 7
x = 3 --> y = 22

 

程式中如果沒有用 nolocal 關鍵字,則程式會報錯,因為 Python 直譯器不能斷定此時的 k 到底是函式 line 內的變數,還是函式 foo 類的區域性變數。