Python變量作用域及閉包

1、引言

最近在刷leetcode題的時候,遇到一個求最長回文子串的題目,于是,我寫了如下的代碼:

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        if len(s) < 2:
            return s
        maxlen = 0
        res = ''
        for i in range(len(s) - 1):
            str1 = self.extendPalidrome(s, i, i)
            str2 = self.extendPalidrome(s, i, i + 1)
            if len(str1) > maxlen:
                res = str1
                maxlen = len(str1)
            if len(str2) > maxlen:
                res = str2
                maxlen = len(str2)
        return res

    def extendPalidrome(self, s, j, k):
        while j >= 0 and k < len(s) and s[j] == s[k]:
            j = j - 1
            k = k + 1
        return s[j + 1:k]


s = Solution()
print (s.longestPalindrome('abccbaaeb'))

哎呀,寫了兩個函數好麻煩啊,想到之前經常有人使用嵌套函數的方式來使代碼變得簡潔,所以我把上述代碼改成了如下的形式:

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        if len(s) < 2:
            return s
        maxlen = 0
        res = ''
        def extendPalidrome(j, k):
            while j >= 0 and k < len(s) and s[j] == s[k]:
                j = j - 1
                k = k + 1
            if k - j - 1 > maxlen:
                maxlen = k - j - 1
                res = s[j+1:k]
        for i in range(len(s) - 1):
            extendPalidrome(i, i)
            extendPalidrome(i, i + 1)
        return res

s = Solution()
print (s.longestPalindrome('abccbaaeb'))

是不是變得簡單了好多,不過報錯了!:

UnboundLocalError: local variable 'maxlen' referenced before assignment

咦,看來這個函數嵌套不能隨便用啊,那么這里正確使用函數嵌套的方式是什么樣的呢?這里我們先賣個關子,重要的不是知道如何解決這個錯誤,而是知道為什么會出現這樣的錯誤,這就需要我們一步步來弄明白。
要解決這個問題,我們要明白python中變量的作用域,以及函數嵌套中變量的作用域。當然,我們也會涉及一些題外話,即Python閉包的知識。

2、變量作用域LEGB

2.1變量的作用域

在Python程序中創建、改變、查找變量名時,都是在一個保存變量名的空間中進行,我們稱之為命名空間,也被稱之為作用域。python的作用域是靜態的,在源代碼中變量名被賦值的位置決定了該變量能被訪問的范圍。即Python變量的作用域由變量所在源代碼中的位置決定。

2.2高級語言對數據類型的使用過程

一般的高級語言在使用變量時,都會有下面4個過程。當然在不同的語言中也會有著區別。

  1. 聲明變量:讓編輯器知道有這一個變量的存在
  2. 定義變量:為不同數據類型的變量分配內存空間
  3. 初始化:賦值,填充分配好的內存空間
  4. 引用:通過引用對象(變量名)來調用內存對象(內存數據)

2.3作用域的產生

就作用域而言,Python與C有著很大的區別,在Python中并不是所有的語句塊中都會產生作用域。只有當變量在Module(模塊)、Class(類)、def(函數)中定義的時候,才會有作用域的概念。看下面的代碼:

#!/usr/bin/env python
def func():
    variable = 100
    print variable
print variable

代碼的輸出為:

NameError: name 'variable' is not defined

在作用域中定義的變量,一般只在作用域中有效。 需要注意的是:在if-elif-else、for-else、while、try-except\try-finally等關鍵字的語句塊中并不會產成作用域。看下面的代碼:

if True:
    variable = 100
    print (variable)
print ("******")
print (variable)

代碼的輸出為:

100
******
100

所以,可以看到,雖然是在if語句中定義的variable變量,但是在if語句外部仍然能夠使用。

2.4作用域的類型:

在Python中,使用一個變量時并不嚴格要求需要預先聲明它,但是在真正使用它之前,它必須被綁定到某個內存對象(被定義、賦值);這種變量名的綁定將在當前作用域中引入新的變量,同時屏蔽外層作用域中的同名變量。

L(local)局部作用域

局部變量:包含在def關鍵字定義的語句塊中,即在函數中定義的變量。每當函數被調用時都會創建一個新的局部作用域。Python中也有遞歸,即自己調用自己,每次調用都會創建一個新的局部命名空間。在函數內部的變量聲明,除非特別的聲明為全局變量,否則均默認為局部變量。有些情況需要在函數內部定義全局變量,這時可以使用global關鍵字來聲明變量的作用域為全局。局部變量域就像一個 棧,僅僅是暫時的存在,依賴創建該局部作用域的函數是否處于活動的狀態。所以,一般建議盡量少定義全局變量,因為全局變量在模塊文件運行的過程中會一直存在,占用內存空間。
注意:如果需要在函數內部對全局變量賦值,需要在函數內部通過global語句聲明該變量為全局變量。

E(enclosing)嵌套作用域

E也包含在def關鍵字中,E和L是相對的,E相對于更上層的函數而言也是L。與L的區別在于,對一個函數而言,L是定義在此函數內部的局部作用域,而E是定義在此函數的上一層父級函數的局部作用域。主要是為了實現Python的閉包,而增加的實現。

G(global)全局作用域

即在模塊層次中定義的變量,每一個模塊都是一個全局作用域。也就是說,在模塊文件頂層聲明的變量具有全局作用域,從外部開來,模塊的全局變量就是一個模塊對象的屬性。
注意:全局作用域的作用范圍僅限于單個模塊文件內

B(built-in)內置作用域

系統內固定模塊里定義的變量,如預定義在builtin 模塊內的變量。

2.5變量名解析LEGB法則

搜索變量名的優先級:局部作用域 > 嵌套作用域 > 全局作用域 > 內置作用域
LEGB法則: 當在函數中使用未確定的變量名時,Python會按照優先級依次搜索4個作用域,以此來確定該變量名的意義。首先搜索局部作用域(L),之后是上一層嵌套結構中def或lambda函數的嵌套作用域(E),之后是全局作用域(G),最后是內置作用域(B)。按這個查找原則,在第一處找到的地方停止。如果沒有找到,則會出發NameError錯誤。
下面舉一個實用LEGB法則的例子:

globalVar = 100           #G

def test_scope():
    enclosingVar = 200    #E
    def func():
        localVar = 300    #L
print __name__            #B

2.6實例講解

下面我們來看幾個例子,加深對于Python變量作用域的理解:

示例1

def func():
    variable = 300
    print variable

variable = 100
func()
print variable

代碼的輸出為:

300
100

本例中,有一個全局變量variable,值為100,有一個作用域為func函數內部的局部變量variable,值為300,func內部輸出variable變量值時,優先搜索局部作用域,所以打印輸出300。

示例2

def test_scopt():
    variable = 200
    print variable
    def func():
        print variable   #這里的變量variable在E中綁定了內存對象200,為函數func()引入了一個新的變量
    func()
variable = 100
test_scopt()
print variable

有兩個variable變量,對于func函數來說,局部作用域中沒有variable變量,所以打印時,在L層找不到,所以進一步在E層找,即在上層函數test_scopt中定義的variable,找到并輸出。

示例3

variable = 300
def test_scopt():
    print variable   #variable是test_scopt()的局部變量,但是在打印時并沒有綁定內存對象。
    variable = 200

test_scopt()
print variable

代碼輸出為:

UnboundLocalError: local variable 'variable' referenced before assignment

上面的例子會報出錯誤,因為在執行程序時的預編譯能夠在test_scopt()中找到局部變量variable(對variable進行了賦值)。在局部作用域找到了變量名,所以不會升級到嵌套作用域去尋找。但是在使用print語句將變量variable打印時,局部變量variable并有沒綁定到一個內存對象(沒有定義和初始化,即沒有賦值)。本質上還是Python調用變量時遵循的LEGB法則和Python解析器的編譯原理,決定了這個錯誤的發生。所以,在調用一個變量之前,需要為該變量賦值(綁定一個內存對象)。
注意:為什么在這個例子中觸發的錯誤是UnboundLocalError而不是NameError:name ‘variable’ is not defined。因為變量variable不在全局作用域。Python中的模塊代碼在執行之前,并不會經過預編譯,但是模塊內的函數體代碼在運行前會經過預編譯,因此不管變量名的綁定發生在作用域的那個位置,都能被編譯器知道。Python雖然是一個靜態作用域語言,但變量名查找是動態發生的,直到在程序運行時,才會發現作用域方面的問題。
這里涉及到了Python編譯運行的原理,我們會在后面進一步學習,本篇暫時不做介紹。

示例4

variable = 300
def test_scopt():
    print variable   #沒有在局部作用域找到變量名,會升級到嵌套作用域尋找,并引入一個新的變量到局部作用域(將局部變量variable賦值為300)。
#    variable = 200

test_scopt()
print variable

代碼輸出為:

300
300

跟示例3進行對比,這里把函數中的賦值語句注釋了,所以打印時直接找到了全局變量variable并輸出。

2.7 不同作用域變量的修改

一個non-L的變量相對于L而言,默認是只讀而不能修改的。如果希望在L中修改定義在non-L的變量,為其綁定一個新的值,Python會認為是在當前的L中引入一個新的變量(即便內外兩個變量重名,但卻有著不同的意義)。即在當前的L中,如果直接使用non-L中的變量,那么這個變量是只讀的,不能被修改,否則會在L中引入一個同名的新變量。這是對上述幾個例子的另一種方式的理解。
注意:而且在L中對新變量的修改不會影響到non-L的。當你希望在L中修改non-L中的變量時,可以使用global、nonlocal關鍵字。

global關鍵字

如果我們希望在L中修改G中的變量,使用global關鍵字。

spam = 99
def tester():
    def nested():
        global spam
        print('current=',spam)
        spam = 200
    return nested
tester()()
print spam

代碼的輸出為:

('current=', 99)
200

上段代碼中,定義了一個內部函數,并作為一個變量返回,所以tester()相當于nested,而不是nested(),所以tester()()相當于nested(),關于函數嵌套的知識我們稍后會講。這里需要注意的是global關鍵字,使用了這個關鍵字之后,在nested函數中使用的spam變量就是全局作用域中的spam變量,而不會新生成一個局部作用域中的spam變量。

nonlocal關鍵字

在L中修改E中的變量。這是Python3.x增加的新特性,在python2.x中還是無法使用。

def outer():
    count = 10
    def inner():
        nonlocal count
        count = 20
        print(count)
    inner()
    print(count)
outer()

輸出為

20
20

由于聲明了nonlocal,這里inner中使用的count變量就是E即outer函數中生命的count變量,所以輸出兩個20。

3、Python函數嵌套

理解了Python中變量的作用域,那么Python函數嵌套就非常容易理解了,我們這里需要注意的一點是Python中的函數也可以當作變量來對待。
python是允許創建嵌套函數的,也就是說我們可以在函數內部定義一個函數,這些函數都遵循各自的作用域和生命周期規則。

def outer():  
    x = 1  
    def inner():  
        print x # 1  
    inner() # 2  
  
outer()  

相信大家都知道輸出是什么了,輸出是1,了解了Python變量的作用域就很容易正確判斷函數嵌套的輸出啦,不過我還是想多啰嗦兩句:
1) #1的地方,python尋找名為x的local變量,在inner作用域內的locals中尋找不到,python就在外層作用域中尋找,其外層是outer函數。x是定義在outer作用域范圍內的local變量。
2) #2的地方,調用了inner函數。這里需要特別注意:inner也只是一個變量名,是遵循python的變量查找規則的(Python先在outer函數的作用域中尋找名為inner的local變量)

4、閉包

閉包的原理我們直接通過下面的例子來解釋:

def outer():  
    x = 1  
    def inner():  
        print x # 1  
    return inner  
foo = outer()  
print foo.func_closure #2 doctest: +ELLIPSIS  
  
foo()  

輸出為:

(<cell at 0x189da2f0: int object at 0x188b9d08>,)
1

在這個例子中,我們可以看到inner函數作為返回值被outer返回,然后存儲在foo變量中,我們可以通過foo()來調用它。但是真的可以跑起來嗎?讓我們來關注一下作用域規則。

python里運行的東西,都按照作用域規則來運行。

  1. x是outer函數里的local變量
  2. 在#1處,inner打印x時,python在inner的locals中尋找x,找不到后再到外層作用域(即outer函數)中尋 找,找到后打印。

看起來一切OK,那么從變量生命周期(lifetime)的角度看,會發生什么呢:

  1. x是outer的local變量,這意味著只有outer運行時,x才存在。那么按照python運行的模式,我們不能在 outer結束后再去調用inner。
  2. 在我們調用inner的時候,x應該已經不存在了。應該發生一個運行時錯誤或者其他錯誤。
    但是這一些都沒有發生,inner函數依舊正常執行,打印了x。

Python支持一種特性叫做函數閉包(function closures):在非全局(global)作用域中定義inner函數(即嵌套函數)時,會記錄下它的嵌套函數namespaces(嵌套函數作用域的locals),可以稱作:定義時狀態,可以通過func_closure 這個屬性來獲得inner函數的外層嵌套函數的namespaces。(如上例中#2,打印了func_closure ,里面保存了一個int對象,這個int對象就是x)

注意:每次調用outer函數時,inner函數都是新定義的。上面的例子中,x是固定的,所以每次調用inner函數的結果都一樣。

如果上面的x不固定呢?我們繼續來看下面的例子:

def outer(x):  
    def inner():  
        print x # 1  
    return inner  
print1 = outer(1)  
print2 = outer(2)  
print print1.func_closure  
print1()  
print print2.func_closure  
print2() 

輸出為:

(<cell at 0x147d3328: int object at 0x146b2d08>,)
1
(<cell at 0x147d3360: int object at 0x146b2cf0>,)
2

在這個例子中,我們能看到閉包實際上是記錄了外層嵌套函數作用域中的local變量。通過這個例子,我們可以創建多個自定義函數。

5、再回首

說了這么多,相信你們都知道文章一開始的錯誤怎么修正了,同時也知道為什么報的UnboundLocalError錯誤了。我們只需要生命nonlocal關鍵詞讓內部函數使用E作用域中的變量就好啦:

class Solution(object):
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        if len(s) < 2:
            return s
        maxlen = 0
        res = ''
        def extendPalidrome(j, k):
            while j >= 0 and k < len(s) and s[j] == s[k]:
                j = j - 1
                k = k + 1
                nonlocal maxlen
                nonlocal res
            if k - j - 1 > maxlen:
                maxlen = k - j - 1
                res = s[j+1:k]
        for i in range(len(s) - 1):
            extendPalidrome(i, i)
            extendPalidrome(i, i + 1)
        return res



s = Solution()
print (s.longestPalindrome('abccbaaeb'))

參考文章:

Python基本語法_變量作用域LEGB:http://blog.csdn.net/jmilk/article/details/50244817
python之嵌套函數與閉包:
http://yunjianfei.iteye.com/blog/2186092

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容