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個過程。當然在不同的語言中也會有著區別。
- 聲明變量:讓編輯器知道有這一個變量的存在
- 定義變量:為不同數據類型的變量分配內存空間
- 初始化:賦值,填充分配好的內存空間
- 引用:通過引用對象(變量名)來調用內存對象(內存數據)
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里運行的東西,都按照作用域規則來運行。
- x是outer函數里的local變量
- 在#1處,inner打印x時,python在inner的locals中尋找x,找不到后再到外層作用域(即outer函數)中尋 找,找到后打印。
看起來一切OK,那么從變量生命周期(lifetime)的角度看,會發生什么呢:
- x是outer的local變量,這意味著只有outer運行時,x才存在。那么按照python運行的模式,我們不能在 outer結束后再去調用inner。
- 在我們調用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