認識Python中的閉包:閉包入門到自閉

本文首發(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,)] 
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辰斋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嘲叔,更是在濱河造成了極大的恐慌亡呵,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硫戈,死亡現(xiàn)場離奇詭異锰什,居然都是意外死亡,警方通過查閱死者的電腦和手機丁逝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門汁胆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人霜幼,你說我怎么就攤上這事嫩码。” “怎么了罪既?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵铸题,是天一觀的道長。 經(jīng)常有香客問我琢感,道長丢间,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任驹针,我火速辦了婚禮烘挫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘柬甥。我一直安慰自己饮六,他們只是感情好其垄,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著卤橄,像睡著了一般绿满。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上窟扑,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天棒口,我揣著相機與錄音,去河邊找鬼辜膝。 笑死,一個胖子當著我的面吹牛漾肮,可吹牛的內(nèi)容都是我干的厂抖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼克懊,長吁一口氣:“原來是場噩夢啊……” “哼忱辅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谭溉,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤墙懂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扮念,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體损搬,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年柜与,在試婚紗的時候發(fā)現(xiàn)自己被綠了巧勤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡弄匕,死狀恐怖颅悉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迁匠,我是刑警寧澤剩瓶,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站城丧,受9級特大地震影響延曙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜芙贫,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一搂鲫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧磺平,春花似錦魂仍、人聲如沸拐辽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俱诸。三九已至,卻和暖如春赊舶,著一層夾襖步出監(jiān)牢的瞬間睁搭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工笼平, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留园骆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓寓调,卻偏偏與公主長得像锌唾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子夺英,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 閉包入門到自閉 冰闊落 前言 python中什么是閉包晌涕?閉包有什么用?為什么要用閉包痛悯?今天我們就帶著這3個問題來一...
    冰闊落jack閱讀 156評論 0 0
  • 我在博客中曾經(jīng)介紹過兩篇關(guān)于函數(shù)的文章余黎,第一篇是 關(guān)于 Python 函數(shù)是第一類對象,第二篇是關(guān)于 Lambda...
    liuzhijun閱讀 1,528評論 2 27
  • (注1:如果有問題歡迎留言探討载萌,一起學習惧财!轉(zhuǎn)載請注明出處,喜歡可以點個贊哦3纯肌)(注2:更多內(nèi)容請查看我的目錄可缚。) ...
    love丁酥酥閱讀 514評論 2 1
  • 久違的晴天,家長會斋枢。 家長大會開好到教室時帘靡,離放學已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗瓤帚。 放學鈴聲...
    飄雪兒5閱讀 7,523評論 16 22
  • 今天感恩節(jié)哎描姚,感謝一直在我身邊的親朋好友。感恩相遇戈次!感恩不離不棄轩勘。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,567評論 0 11