Python入門系列26 - 進(jìn)階必修之閉包(一)
Python入門系列26

進(jìn)階必修之閉包(一)
本篇閱讀時(shí)間約為8分鐘。
1
前言
Python入門系列20 - 5分鐘教你用圖片定位具體地址!
2
再談函數(shù)
講真的,閉包從概念講是非常難講明白的一個(gè)詞。所以,先來(lái)搞清楚函數(shù)在Python里是怎樣的存在,深入了解了函數(shù)的概念,會(huì)為學(xué)習(xí)閉包打下基礎(chǔ)。閉包與函數(shù)有著密不可分的關(guān)系。
通常,在一般的編程語(yǔ)言中,函數(shù)只是一段可執(zhí)行的邏輯代碼,并不是所謂的對(duì)象。比如在Java中,如下代碼,打印輸出一句話:
/**
*?函數(shù)描述:說(shuō)一句話
*?public?:?共有方法
* void ??:沒(méi)有返回類型
*?@param?words?參數(shù),傳入的話語(yǔ)
*/
public?static?void?say(String?words)?{
? ? System.out.println(words);
}
public?static?void?main(String[]?args)?{
? ? say("你好");
}
像Java這種有明確類型定義聲明的語(yǔ)言,如果沒(méi)有設(shè)置返回類型,在調(diào)用函數(shù)時(shí),則不能用任何變量去接受,否則編譯時(shí)就會(huì)報(bào)錯(cuò)。(只是作對(duì)比的例子,非專業(yè)人員忽略即可)。
再來(lái)看下Python中呢,若我們像打印一句話,也需要定義一個(gè)函數(shù),然后調(diào)用即可,同時(shí),我們可以通過(guò)任何變量來(lái)將此函數(shù)進(jìn)行賦值操作,如下:
#?打印一句話
def?say(words):
????print(words)
say('你好')
a?=?say
print(type(a))

結(jié)果顯示,在Python中,是可以用函數(shù)不加小括號(hào)的形式將其賦值給任何變量,并且其自身也可以作為函數(shù)的參數(shù)進(jìn)行傳遞,不僅如此,函數(shù)也可以用此種方式將其自身作為返回結(jié)果進(jìn)行返回,通過(guò)上面的代碼可以看到,say賦值給變量a,打印a的類型,得到class?function,說(shuō)明它是一個(gè)類。之前的小課堂中提及過(guò),在Python中一切皆為對(duì)象,函數(shù)也不例外!
可能有人會(huì)說(shuō),你在Java中不是這么寫(xiě)的啊,你也像在Python中那樣調(diào)用試下,看看結(jié)果唄,于是有了下面的圖:

可以看到,當(dāng)在Java中寫(xiě)say不加小括號(hào)時(shí),編譯器已經(jīng)報(bào)錯(cuò)了,提示找不到say的標(biāo)識(shí)符(會(huì)Java的人都知道這么寫(xiě)是沒(méi)有意義的)。這也是Java與Python作為編程語(yǔ)言的不同之處,一個(gè)為編譯型語(yǔ)言,一個(gè)為解釋型語(yǔ)言。
正因?yàn)镻ython有了這么一個(gè)特性,所以才會(huì)很好地支持接下來(lái)要介紹的閉包!
3
什么是閉包?
在解釋概念之前,先來(lái)看個(gè)自帶場(chǎng)景的小例子吧!不知道大家還記不記得小學(xué)(是小學(xué)還是初中來(lái)著,忘記了...默認(rèn)小學(xué)吧?。W(xué)過(guò)的一個(gè)數(shù)學(xué)公式,如何去求圓的面積?記憶好的一定記得:
,r為圓的半徑,π為3.1415926.....
現(xiàn)在的場(chǎng)景是,需要定義一個(gè)求圓形面積的函數(shù),同時(shí),在這個(gè)函數(shù)的外部還要包裹著一層求圓形面積之前提前做準(zhǔn)備的外層函數(shù),這外層函數(shù)的目的是你可以在真正求圓形面積前演算一些內(nèi)容(假設(shè)大家都是剛學(xué)這個(gè)公式的小學(xué)生喲!
),寫(xiě)法分解成以下步驟:
1. 定義兩個(gè)函數(shù),并且在調(diào)用內(nèi)部計(jì)算圓形面積的函數(shù)
def?circular_area_pre():
????def?circular_area():
????????print('This?is?circular_area?function!')其中 circular_area_pre 是計(jì)算圓形面積前的準(zhǔn)備函數(shù),circular_area 是計(jì)算圓形面積的函數(shù)。此時(shí)若想在外面直接調(diào)用?circular_area 如何做呢?(不要感到這種寫(xiě)法奇怪,Python中是可以進(jìn)行函數(shù)嵌套的?。﹪L試下自己手動(dòng)調(diào)用!如下圖:

當(dāng)嘗試在外層直接進(jìn)行調(diào)用時(shí),可以看到,已經(jīng)報(bào)錯(cuò)了!如何正確調(diào)用呢?在上面的標(biāo)題「再談函數(shù)」中說(shuō)過(guò),函數(shù)可以通過(guò)“對(duì)象”的寫(xiě)法將其自身作為結(jié)果進(jìn)行返回!(忘記的往上翻翻,找找看!)所以改下寫(xiě)法如下:
def?circular_area_pre():
????def?circular_area():
????????print('This?is?circular_area?function!')
????return?circular_area()
c?=?circular_area_pre()
c()
通過(guò)調(diào)用外層的計(jì)算準(zhǔn)備函數(shù) circular_area_pre ,函數(shù)返回接受到的內(nèi)部計(jì)算圓形面積函數(shù) circular_area 作為變量c,以函數(shù)的形式調(diào)用變量c(也就是在c后面加上小括號(hào)進(jìn)行函數(shù)調(diào)用),執(zhí)行即可!而此時(shí)的變量c實(shí)際上是一個(gè)函數(shù)(這點(diǎn)在后面的步驟中會(huì)進(jìn)行驗(yàn)證,先記住。)!執(zhí)行一下,咦???發(fā)現(xiàn)報(bào)錯(cuò)了,因?yàn)槎嗔藗€(gè)小括號(hào):

去掉后成功,所以一定要注意小括號(hào)的問(wèn)題?。∪缦聢D :

2. 在第一步的基礎(chǔ)上,將其補(bǔ)充完整,套入公式
def?circular_area_pre():????"""?省略了一些演算步驟的代碼,畢竟假設(shè)嘛?"""
????pai?=?3.14
????def?circular_area(r):
????????s?=?pai?*?r?**?2
????????return?s
????return?circular_area
c?=?circular_area_pre()
print(c(10))
解釋下這段代碼的含義,將pai(π)定義為3.14,放在作為計(jì)算圓形面積之前的函數(shù) circular_area_pre?中,而求面積的公式則寫(xiě)在 circular_area 函數(shù)中,將面積變量s作為內(nèi)部函數(shù)返回,同時(shí),最外層的?circular_area_pre 函數(shù)返回?circular_area 函數(shù)作為結(jié)果。想要計(jì)算出圓形的面積,在外層就需要先調(diào)用?circular_area_pre 準(zhǔn)備函數(shù),在調(diào)用其返回結(jié)果變量c,打印 c(10),可以看到如下圖結(jié)果:

打印結(jié)果輸出的是314.0,實(shí)際上就是將10傳入到了?circular_area 函數(shù)中,而其中的pai引用的是在?circular_area_pre 局部里定義的pai,值為3.14 。驗(yàn)證下步驟1說(shuō)的,看下變量c的類型,以及直接打印c會(huì)出現(xiàn)什么樣子的結(jié)果:

現(xiàn)在可以看到,其實(shí)變量c就是一個(gè)類,而它的類型是function.?
3. 關(guān)于 pai 的定義位置
拋開(kāi)外層的準(zhǔn)備函數(shù),假設(shè)現(xiàn)在只有計(jì)算圓形的函數(shù),假設(shè)阿基米德突然復(fù)活了,把這個(gè)pai推算成了其它數(shù)字!姑且定義為10吧,如下:
pai?=?3.14
def?circular_area(r):
????s?=?pai?*?r?**?2
????return?s
pai?=?10
print(circular_area(10))

輸出結(jié)果為1000,記住這個(gè)值,咱們繼續(xù)往下看!
4. 如果此時(shí)在外部修改了pai的值,打印結(jié)果如何?
回到雙層函數(shù)的示例來(lái),在?circular_area 函數(shù)中,pai是沒(méi)有被定義的,所以它會(huì)向上一層尋找,于是找到了?circular_area_pre 函數(shù)中定義的pai,所以計(jì)算的時(shí)候值就會(huì)取為3.14,假設(shè)現(xiàn)在依然在最外層函數(shù)的外面修改pai的值,繼續(xù)改為10,那么代碼的結(jié)果會(huì)是如何呢?

來(lái)看下:
def?circular_area_pre():
????"""?省略了一些演算步驟的代碼,畢竟假設(shè)嘛?"""
????pai?=?3.14
????def?circular_area(r):
????????s?=?pai?*?r?**?2
????????return?s
????return?circular_area
pai?=?10
c?=?circular_area_pre()
print(c(10))
在調(diào)用準(zhǔn)備函數(shù)之前,將pai修改為10!猜猜看,結(jié)果會(huì)打印出什么呢?(先不要看結(jié)果,自己思考一下!)結(jié)果如下:

沒(méi)錯(cuò),你沒(méi)有看錯(cuò),依然是314.0,這個(gè)結(jié)果與沒(méi)加入pai = 10 的代碼得到的結(jié)果是一樣的!為什么不是1000呢???請(qǐng)繼續(xù)往后看!
5. 閉包的概念
啰里啰嗦的舉例了這么多,到底跟今天要說(shuō)的閉包有什么關(guān)系呢!各位看官,莫急,下面就是重頭戲了,只要你耐心的把上面的示例都看懂,保證你看完接下來(lái)的這段概念性文字一目了之!
在上面的函數(shù)?circular_area 與 pai = 3.14 形成了閉包!通俗的說(shuō)就是把內(nèi)部函數(shù) circular_area 與?pai = 3.14 這個(gè)環(huán)境變量包含在了一起,做了一個(gè)封閉,所以外界想要去改變 pai 這個(gè)變量是改變不了的!
需要注意的是,所謂的環(huán)境變量,一定要定義在內(nèi)部函數(shù)的外部,就像 pai 一樣,且不能是全局變量!
閉包的概念:閉包 = 函數(shù) + 環(huán)境變量!
6. Python函數(shù)作用域
Python變量的作用域一共有4種,分別是:
L (Local) 局部作用域
E (Enclosing) 閉包函數(shù)外的函數(shù)中
G (Global) 全局作用域
B (Built-in) 內(nèi)建作用域
以 L –> E –> G –>B 的規(guī)則查找,即:在局部找不到,便會(huì)去局部外的局部找(例如閉包),再找不到就會(huì)去全局找,再者去內(nèi)建中找。
通過(guò)這段作用域的概念,可以得知,為什么在求面積的函數(shù)外部去修改pai的值,最終取到的還是原來(lái)的3.14,Python的變量查詢是一個(gè)“由內(nèi)到外”的過(guò)程,一旦找到,則取最內(nèi)部的變量進(jìn)行使用。
4
如何查看一個(gè)函數(shù)是否閉包
閉包是可以通過(guò)python內(nèi)置函數(shù)檢測(cè)出來(lái)的,如下:
def?circular_area_pre():
????"""?省略了一些演算步驟的代碼,畢竟假設(shè)嘛?"""
????pai?=?3.14
????def?circular_area(r):
????????s?=?pai?*?r?**?2
????????return?s
????return?circular_area
pai?=?10
c?=?circular_area_pre()
print(c.__closure__)
print(c.__closure__[0].cell_contents)

通過(guò)調(diào)用 __closure__ 內(nèi)置方法可以查看到兩個(gè)內(nèi)存地址,結(jié)果返回cell就是閉包,None 則不是閉包,可以看出來(lái)其實(shí)這是一個(gè)元組類型,使用[0].cell_contents可以得到閉合數(shù)值,也就閉包所需要的環(huán)境變量。
小題,請(qǐng)你判斷下面這段代碼屬于閉包嗎?如下:
def?circular_area_pre():
????"""?省略了一些演算步驟的代碼,畢竟假設(shè)嘛?"""
????def?circular_area(r):
????????pai?=?3.14
????????s?=?pai?*?r?**?2
????????return?s
????return?circular_area
c?=?circular_area_pre()
print(c.__closure__)
print(c.__closure__[0].cell_contents)
答案:

只把pai 進(jìn)行了換位,導(dǎo)致現(xiàn)在的寫(xiě)法并不是閉包,調(diào)用Python內(nèi)置方法來(lái)查看,得到的是None,沒(méi)獲取到閉包時(shí)產(chǎn)生的cell!
5
總結(jié)
說(shuō)真的,本章的閉包要想講明白真的挺難得,這篇文章大概是花了一星期的時(shí)間去寫(xiě)的,基本上基礎(chǔ)的概念介紹的差不多了,但是要想懂閉包,還是得看懂示例才行,記住閉包就是函數(shù)和環(huán)境變量封閉組成的!外界想改環(huán)境變量?沒(méi)門!
后續(xù)還有一篇閉包二,會(huì)講述閉包的使用場(chǎng)景,究竟什么時(shí)候使用閉包才是合適的。敬請(qǐng)期待.....
至此完!
