溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Python 中閉包概念是什么

發(fā)布時間:2021-07-06 12:01:40 來源:億速云 閱讀:115 作者:chen 欄目:編程語言

本篇內(nèi)容介紹了“Python 中閉包概念是什么”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!


 

概要

計算機中有些英文專業(yè)詞匯,字面直譯,難免因缺少上下文而顯得蒼白拗口,須得多方鋪墊,方能味得古怪下面的原理。閉包(closure)便是一個這樣牽扯了許多上下文的概念,包括編程語言最基本的綁定(binding),環(huán)境(environments),變量作用域(scope)以及函數(shù)是第一等公民(function as the first-class)等等。

 

Binding(綁定)

在Python中,binding(綁定) 是編程語言最基本的抽象手法,它將一個值綁定到一個變量上,并且稍后可以引用或者修改該變量。下面是幾種不同層次的綁定,每組語句在運行時將一個名字與對應(yīng)值綁定到其定義所在的環(huán)境中。

  • 將名字綁定到一塊內(nèi)存,通過賦值語句實現(xiàn),當(dāng)然函數(shù)調(diào)用時,形參和實參結(jié)合也是綁定:
In [1]: square = 4
 
  • 將名字綁定到一組復(fù)合運算,即     函數(shù)定義,利用     def 關(guān)鍵字實現(xiàn):
In [1]: def square(x):
            return x*x
 
  • 將名字綁定到一個數(shù)據(jù)集合,即     類定義,使用     class 實現(xiàn):
In [1]: class square:
            def __init__(self, x):
                self.x = x

            def value(self):
                return self.x * self.x
 

依照執(zhí)行順序,同名多次綁定,后面會覆蓋前面

In [1]: square = 3

In [2]: square
Out[2]: 3

In [3]: def square(x):
   ...:     return x * x
   ...:
   ...:

In [4]: square
Out[4]: <function __main__.square(x)>

In [5]: class square:
   ...:     def __init__(self, x):
   ...:         self.x = x
   ...:

In [6]: square
Out[6]: __main__.square
 

說這些都是抽象,是因為它們提供了對數(shù)據(jù)、復(fù)合操作或數(shù)據(jù)集合的封裝手段,即將一個名稱與復(fù)雜的數(shù)據(jù)或邏輯進行捆綁,使調(diào)用者不用關(guān)心其實現(xiàn)細節(jié),并可以據(jù)此來構(gòu)建更復(fù)雜的工程??梢哉f綁定是編程的基石。

回到本文的主題上來,閉包是對一組復(fù)合語句的抽象,也就是函數(shù),只不過是一種特殊的函數(shù),至于這個特殊性在哪,這里先賣個關(guān)子,等稍后引入更多概念后再進行闡述。

 

Scope (作用域)

scope(作用域),顧名思義,也就是某個binding 能罩多大的范圍,或者說可以在多大范圍內(nèi)訪問的到一個變量。每個函數(shù)定義會生成一個局部定義域。

Python,和大多數(shù)編程語言一樣,使用的是靜態(tài)作用域(static scoping,有時也稱 lexical scoping)規(guī)則。在函數(shù)嵌套定義的時候,內(nèi)層函數(shù)內(nèi)可以訪問外層函數(shù)的變量值。因此你可以把作用域想象成一個容器,即它是可以嵌套的,并且內(nèi)層作用域會擴展外層作用域,而最外層作用域即全局作用域。

上一小節(jié)提到了,多次同名綁定,后面會覆蓋先前,其實有隱含前提:在同一作用域內(nèi)。如果是嵌套作用域,其實是隱藏的關(guān)系,內(nèi)層函數(shù)的變量定義會遮蔽外層函數(shù)同一名字定義,但是在外層作用域中,該變量仍是原值:

In [16]: a = 4

In [17]: def outer(): 
    ...:     a = 5
    ...:     print(a)
    ...:     def inner():
    ...:         a = 6
    ...:         print(a)
    ...:     inner()
    ...:     print(a)
    ...:

In [18]: outer()
5
6
5

In [19]: print(a)
4
 

作用域其實也可以從另一個角度理解,即我們在某個環(huán)境(environment)中,在確定一個name binding 值的時候,會從最內(nèi)層作用域順著往外找,找到的第一個該名字 binding 的對應(yīng)的值即為該 name 引用到的值。

需要強調(diào)的時候,函數(shù)的嵌套定義會引起定義域的嵌套,或者說環(huán)境擴展(內(nèi)層擴展外層)關(guān)系。類的定義又稍有不同,class 定義會引入新的 namespace(命名空間),命名空間和作用域是常拿來對比的概念,但這里按下不表,感興趣的可以自己去查查資料。

說到這里,要提一下,一個常被說起的反直覺例子:

In [50]: a = 4

In [51]: def test():
    ...:     print(a) # 這里應(yīng)該輸出什么?
    ...:     a = 5
    ...:

In [52]: test()
---------------------------------------------------------------------------
UnboundLocalError                         
Traceback (most recent call last)
<ipython-input-52-fbd55f77ab7c> in <module>()
----> 1 test()

<ipython-input-51-200f78e91a1b> in test()
      1 def test():
----> 2     print(a)
      3     a = 5
      4

UnboundLocalError: local variable 'a' referenced before assignment
 

想象中,上面 print 處應(yīng)該輸出 4 或者 5 才對,為什么會報錯呢?這是因為 test 函數(shù)在被解釋器解析的時候,分詞器會掃一遍 test 函數(shù)定義中的所有 token(符號),看到賦值語句 a=5 的存在,就會明確 a 是一個局部變量,因此不會輸出 4。而在執(zhí)行到 print(a) 的時候,在局部環(huán)境中,a 還未被binding,因此會報 UnboundLocalError。

稍微擴展說明一下,雖然 Python 是解釋執(zhí)行的,即輸入一句,解釋一句,執(zhí)行一句。但是對于代碼塊(即頭部語句,冒號與其關(guān)聯(lián)的縮進塊所構(gòu)成的復(fù)合語句(compound sentences),常見的有函數(shù)定義,類定義,循環(huán)語句等等)來說,還是會整體先掃一遍的。

 

First-Class Function(函數(shù)是第一等公民)

一般來說,組成編程語言的元素,如變量、函數(shù)和類,會被設(shè)定不同的限制,而具有最少限制的元素,被我們稱為該編程語言中的一等公民。而一等公民最常見的特權(quán)有:

  1. 可以被     綁定到名字上
  2. 可以作為參數(shù)在函數(shù)中傳遞
  3. 可以作為返回值被函數(shù)作為結(jié)果返回
  4. 可以被包含在其他數(shù)據(jù)結(jié)構(gòu)中

套用到 Python 中的函數(shù),即一個函數(shù)可以被賦值給某個變量,可以被其他函數(shù)接收和返回,可以定義在其他函數(shù)中(即嵌套定義):

In [32]: def test():
    ...:     print('hello world')
    ...:

In [33]: t = test # 賦值給變量

In [34]: t()
hello world

In [35]: def wrapper(func):
    ...:     print('wrapper')
    ...:     func()
    ...:

In [36]: wrapper(t) # 作為參數(shù)傳遞
wrapper
hello world

In [37]: def add_num(a): 
    ...:     def add(b): # 嵌套定義
    ...:         return a + b
    ...:     return add # 作為函數(shù)的返回值
    ...:
    ...:

In [38]: add5 = add_num(5)

In [39]: add5(4)
Out[39]: 9
 

并不是在所有語言中,函數(shù)都是一等公民,比如 Java8 以前的 Java,上面四項權(quán)利 Java7 中的函數(shù)后幾項都沒有。使用函數(shù)作為第一等公民的做法,我們成為函數(shù)式編程。在這個大數(shù)據(jù)時代,由于對并發(fā)的友好性,傳統(tǒng)過程式語言(比如 Cpp、Java)都在新版本上逐漸支持函數(shù)式編程范式。

在這里,能夠操作其他函數(shù)的函數(shù)(即以其他函數(shù)作為參數(shù)或者返回值的函數(shù)),叫做高階函數(shù)。高階函數(shù)使得語言的表達能力大大增強,但同時,也增加了編程復(fù)雜度。

 

Stack Call(棧式調(diào)用)

每個函數(shù)調(diào)用,會在環(huán)境中產(chǎn)生一個 frame棧幀),并且在棧幀中會進行一些綁定,然后壓入函數(shù)調(diào)用棧中。在函數(shù)調(diào)用結(jié)束時,棧幀會被彈出,其中所進行的綁定也被解除,即垃圾回收,對應(yīng)的局部作用域也隨之消亡。

In [47]: def test():
    ...:     x = 4
    ...:     print(x)
    ...:

In [48]: test()
4

In [49]: x
---------------------------------------------------------------------------
NameError                                 
Traceback (most recent call last)
<ipython-input-49-6fcf9dfbd479> in <module>()
----> 1 x

NameError: name 'x' is not defined
 

即在調(diào)用結(jié)束后,局部定義的變量  x 在外邊是訪問不到的。但是如之前例子中,返回的 add 函數(shù)卻引用了已經(jīng)調(diào)用結(jié)束的 add_num 中的變量 a,怎么解釋這種現(xiàn)象呢?可以記住一條,也是之前提到過的:

函數(shù)嵌套定義時,內(nèi)部定義的函數(shù)所在的環(huán)境會自動擴展其定義所在環(huán)境
 

因此在外部函數(shù)返回后,返回的內(nèi)部函數(shù)依然維持了其定義時的擴展環(huán)境,也可以理解為由于內(nèi)部函數(shù)引用的存在,外部函數(shù)的環(huán)境中所有的綁定并沒有被回收。

 

Closure(閉包)

千呼萬喚始出來,以為是高潮,其實已結(jié)束。

閉包就是建立在前面的這些概念上的,上面提到的某個例子:

In [37]: def add_num(a): 
    ...:     def add(b): # 嵌套定義
    ...:         return a + b
    ...:     return add # 作為函數(shù)的返回值
    ...:
    ...:

In [38]: add5 = add_num(5)

In [39]: add5(4)
Out[39]: 9
 

其實就是閉包。撿起之前伏筆,給出我對閉包的一個理解:它是一種高階函數(shù),并且外層函數(shù)(例子中的add_num)將其內(nèi)部定義的函數(shù)(add)作為返回值返回,同時由于返回的內(nèi)層函數(shù)擴展了外層函數(shù)的環(huán)境,也就是對其產(chǎn)生了一個引用,那么在調(diào)用返回的內(nèi)部函數(shù)(add5)的時候,能夠引用到其(add)定義時的外部環(huán)境(在例子中,即 a 的值)。

結(jié)語

說了這么多,其實只是在邏輯層面或者說抽象層面去解釋閉包是什么,常跟哪些概念糾纏在一起。但這些都沒有真正觸到其本質(zhì),或者說依然是空中樓閣,如果想要真正理解閉包,可以去詳細了解下 Python 的解釋執(zhí)行機制,當(dāng)然,那就是編譯原理的范疇了。

“Python 中閉包概念是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI