閉包(closure
)是函數式編程的重要的語法結構。函數式編程是一種編程范式 (而面向過程編程和面向對象編程也都是編程范式)。
- 在面向過程編程中,我們見到過函數(
function
); - 在面向對象編程中,我們見過對象(
object
)。
函數和對象的根本目的是以某種邏輯方式組織代碼,并提高代碼的可重復使用性(reusability
)。閉包也是一種組織代碼的結構,它同樣提高了代碼的可重復使用性。
不同的語言實現閉包的方式不同。Python以函數對象為基礎,為閉包這一語法結構提供支持的。Python一切皆對象,函數這一語法結構也是一個對象。在函數對象中,我們像使用一個普通對象一樣使用函數對象,比如更改函數對象的名字,或者將函數對象作為參數進行傳遞。
函數對象的作用域
和其他對象一樣,函數對象也有其存活的范圍,也就是函數對象的作用域。函數對象是使用def語句定義的,函數對象的作用域與def所在的層級相同。比如下面代碼,我們在line_conf
函數的隸屬范圍內定義的函數line,就只能在line_conf
的隸屬范圍內調用。
def line_conf():
def line(x):
return 2*x+1
print(line(5)) # within the scope
line_conf()
print(line(5)) # out of the scope
line
函數定義了一條直線(y = 2x + 1)。可以看到,在line_conf()
中可以調用line
函數,而在作用域之外調用line將會有下面的錯誤: NameError: name ‘line’ is not defined
閉包
函數是一個對象,所以可以作為某個函數的返回結果。
def line_conf():
def line(x):
return 2*x+1
return line # return a function object
my_line = line_conf()
print(my_line(5))
上面的代碼可以成功運行。line_conf
的返回結果被賦給line
對象。上面的代碼將打印11
。如果line()
的定義中引用了外部的變量,會發生什么呢?
def line_conf():
b = 15
def line(x):
return 2*x+b
return line # return a function object
b = 5
my_line = line_conf()
print(my_line(5))
我們可以看到,line
定義的隸屬程序塊中引用了外層變量b
,b
信息存在于line
的定義之外。我們稱b
為line
的環境變量。事實上,line
作為line_conf
的返回值時,line
中已經包括b
的取值(盡管b
并不隸屬于line
)。
上面的代碼將打印25
,也就是說,line
所參照的b
值是函數對象定義時可供參考的b
值,而不是使用時的b
值。
一個函數和它的環境變量合在一起,就構成了一個閉包(closure)。在Python中,所謂的閉包是一個包含有環境變量取值的函數對象。環境變量取值被保存在函數對象的__closure__
屬性中。比如下面的代碼:
def line_conf():
b = 15
def line(x):
return 2*x+b
return line # return a function object
b = 5
my_line = line_conf()
print(my_line.__closure__)
print(my_line.__closure__[0].cell_contents)
__closure__
里包含了一個元組(tuple)。這個元組中的每個元素是cell類型的對象。我們看到第一個cell包含的就是整數15,也就是我們創建閉包時的環境變量b的取值。
例子
def line_conf(a, b):
def line(x):
return ax + b
return line
line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5), line2(5))
這個例子中,函數line
與環境變量a,b構成閉包。環境變量由外層函數給出。在創建閉包的時候,我們通過line_conf
的參數a,b給出了這兩個環境變量的取值,這樣,我們就確定了函數的最終形式(y = x + 1和y = 4x + 5)。我們只需要變換參數a,b,就可以獲得不同的直線表達函數。由此,我們可以看到,閉包也具有提高代碼可復用性的作用。
如果沒有閉包,我們需要每次創建直線函數的時候同時說明a,b,x。這樣,我們就需要更多的參數傳遞,也減少了代碼的可移植性。利用閉包,我們實際上創建了泛函。line
函數定義一種廣泛意義的函數。這個函數的一些方面已經確定(必須是直線),但另一些方面(比如a和b參數待定)。隨后,我們根據line_conf
傳遞來的參數,通過閉包的形式,將最終函數確定下來。