本文首發(fā)于:行者AI
python
中什么是閉包?閉包有什么用闻镶?為什么要用閉包?今天我們就帶著這3個問題來一步一步認識閉包。
閉包和函數(shù)緊密聯(lián)系在一起杆故,介紹閉包前有必要先介紹一些背景知識豆茫,諸如嵌套函數(shù)侨歉、變量的作用域等概念。
1. 作用域
作用域是程序運行時變量可被訪問的范圍揩魂,定義在函數(shù)內(nèi)的變量是局部變量幽邓,局部變量的作用范圍只能是函數(shù)內(nèi)部范圍內(nèi),它不能在函數(shù)外引用火脉。
定義在模塊最外層的變量是全局變量牵舵,它是全局范圍內(nèi)可見的柒啤,當然在函數(shù)里面也可以讀取到全局變量的。而在函數(shù)外部則不可以訪問局部變量畸颅。例如:
a = 1
def foo():
print(a) # 1
def foo():
print(a) # NameError: name 'num' is not defined
2. 嵌套函數(shù)
函數(shù)不僅可以定義在模塊的最外層担巩,還可以定義在另外一個函數(shù)的內(nèi)部,像這種定義在函數(shù)里面的函數(shù)稱之為嵌套函數(shù)(nested function)
没炒。對于嵌套函數(shù)涛癌,它可以訪問到其外層作用域中聲明的非局部(non-local)
變量,比如代碼示例中的變量a
可以被嵌套函數(shù)printer
正常訪問送火。
def foo():
#foo是外圍函數(shù)
a = 1
# printer是嵌套函數(shù)
def printer():
print(a)
printer()
foo() # 1
那么有沒有一種可能即使脫離了函數(shù)本身的作用范圍拳话,局部變量還可以被訪問得到呢?
答案就是閉包漾脂!
我們將上述函數(shù)改成高階函數(shù)(接受函數(shù)為參數(shù)假颇,或者把函數(shù)作為結(jié)果返回的函數(shù)是高階函數(shù))的寫法。
def foo():
#foo是外圍函數(shù)
a = 1
# printer是嵌套函數(shù)
def printer():
print(a)
return printer
x = foo()
x() # 1
這段代碼和前面例子的效果完全一樣骨稿,同樣輸出 1
笨鸡。不同的地方在于內(nèi)部函數(shù) printer
直接作為返回值返回了。
一般情況下坦冠,函數(shù)中的局部變量僅在函數(shù)的執(zhí)行期間可用形耗,一旦 foo()
執(zhí)行過后,我們會認為變量a
將不再可用辙浑。然而激涤,在這里我們發(fā)現(xiàn) foo
執(zhí)行完之后,在調(diào)用x
的時候a
變量的值正常輸出了判呕,這就是閉包的作用倦踢,閉包使得局部變量在函數(shù)外被訪問成為可能。
3. 閉包
人們有時會把閉包和匿名函數(shù)弄混侠草。這是有歷史原因的:在函數(shù)內(nèi)部定義函數(shù) 不常見辱挥,直到開始使用匿名函數(shù)才會這樣做。而且边涕,只有涉及嵌套函數(shù)時才有閉包問題晤碘。 因此,很多人是同時知道這兩個概念的功蜓。
其實园爷,閉包指延伸了作用域的函數(shù),其中包含函數(shù)定義體中引用式撼、但是不在定義體中定義的 非全局變量童社。函數(shù)是不是匿名的沒有關(guān)系,關(guān)鍵是它能訪問定義體之外定義的非全局變量著隆。
通俗來講閉包叠洗,顧名思義甘改,就是一個封閉的包裹,里面包裹著自由變量灭抑,就像在類里面定義的屬性值一樣,自由變量的可見范圍隨同包裹抵代,哪里可以訪問到這個包裹腾节,哪里就可以訪問到這個自由變量。 那這個包裹是綁定在哪的呢荤牍?在上文代碼追加一句打影赶佟:
def foo():
# foo是外圍函數(shù)
a = 1
# printer是嵌套函數(shù)
def printer():
print(a)
return printer
x = foo()
print(x.__closure__[0].cell_contents) # 1
可以發(fā)現(xiàn)是在函數(shù)對象的__closure__
屬性中,__closure__
是一個元祖對象函數(shù)負責閉包綁定康吵,即自由變量的綁定劈榨。該屬性值通常是None
,如果這個函數(shù)是一個閉包的話晦嵌,那么它返回的是一個由cell
對象組成的元組對象同辣。cell
對象的cell_contents
屬性就是閉包中的自由變量。這解釋了為什么局部變量脫離函數(shù)之后惭载,還可以在函數(shù)之外被訪問的原因的旱函,因為它存儲在了閉包的 cell_contents
中了。
4. 閉包的好處
閉包避免了使用全局變量描滔,此外棒妨,閉包允許將函數(shù)與其所操作的某些數(shù)據(jù)(環(huán)境)關(guān)連起來。這一點與面向?qū)ο缶幊淌欠浅n愃频暮ぃ诿鎸ο缶幊讨腥唬瑢ο笤试S我們將某些數(shù)據(jù)(對象的屬性)與一個或者多個方法相關(guān)聯(lián)。
一般來說拘泞,當對象中只有一個方法時纷纫,這時使用閉包是更好的選擇。來看一個計算均值的例子田弥,假如有個名為avg
的函數(shù)涛酗,它的作用是計算不斷增加的系列值的均值;例如,整個歷史中 某個商品的平均收盤價偷厦。每天都會增加新價格商叹,因此平均值要考慮至目前為止所有的價格,如下所示:
>>> avg(10) #10.0
>>> avg(11) #10.5
>>> avg(12) #11.0
在以往只泼,我們可以設(shè)計一個類:
class Averager():
def __init__(self):
self.series = []
def __call__(self, new_value):
self.series.append(new_value)
total = sum(self.series)
return total/len(self.series)
avg = Averager()
avg(10) #10.0
avg(11) #10.5
avg(12) #11.0
這時候我們使用閉包來實現(xiàn)剖笙。
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
avg = make_averager()
avg(10) #10.0
avg(11) #10.5
avg(12) #11.0
調(diào)用make_averager
時,返回一個 averager
函數(shù)對象请唱。每次調(diào)用 averager
時弥咪,它會把參數(shù)添加到列表中过蹂,然后計算當前平均值。 這比用類來實現(xiàn)更優(yōu)雅聚至,此外裝飾器也是基于閉包的一中應(yīng)用場景酷勺。
5. 閉包的坑
看了上述閉包的解釋你以為閉包也不過如此?實際使用中往往在不經(jīng)意間就會掉入陷阱扳躬,看看下面的例子:
def create_multipliers():
return [lambda x: x * i for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 期望輸出0, 2, 4, 6, 8
# 結(jié)果是 8, 8, 8, 8, 8
我們期望是輸出0, 2, 4, 6, 8
脆诉。結(jié)果卻是8, 8, 8, 8, 8
。為什么會出現(xiàn)這問題呢贷币?讓我們改下代碼:
def create_multipliers():
multipliers = [lambda x: x * i for i in range(5)]
print([m.__closure__[0].cell_contents for m in multipliers])
create_multipliers() # [4, 4, 4, 4, 4]
可以看到函數(shù)綁定的i
值都成了4
即循環(huán)后最終i的取值击胜,這是因為Python
的閉包是延遲綁定 ,這意味著閉包中用到的變量的值役纹,是在內(nèi)部函數(shù)被調(diào)用時查詢得到的偶摔。
正確的使用方式是將i的值利用參數(shù)的方式進行傳遞:
def create_multipliers():
return [lambda x,i=i: x * i for i in range(5)]
s = create_multipliers()
for multiplier in s:
print(multiplier(2)) # 0, 2, 4, 6, 8
我們利用默認參數(shù)來傳遞i
,同閉包一樣默認參數(shù)是綁定在__defaults__
屬性上促脉。
print([f.__defaults__ for f in s]) # [(0,), (1,), (2,), (3,), (4,)]