Python中名稱空間與作用域使用總結
1 引言
2 名稱空間
2.1 什麼是名稱空間
名稱空間,即Namespace,也成為名稱空間或名字空間,指的是從名字到物件的一個對映關係,類似於字典中的鍵值對,實際上,Python中很多名稱空間的實現用的就是字典。
不同名稱空間是相互獨立的,沒有任何關係的,所以同一個名稱空間中不能有重名,但不同的名稱空間是可以重名而沒有任何影響。
2.2 名稱空間的型別
Python名稱空間按照變數定義的位置,可以劃分為以下3類:
Built-in,內建名稱空間,python自帶的內建名稱空間,任何模組均可以訪問,存放著內建的函式和異常。
Global,全域性名稱空間,每個模組載入執行時建立的,記錄了模組中定義的變數,包括模組中定義的函式、類、其他匯入的模組、模組級的變數與常量。
Local,區域性名稱空間,每個函式、類所擁有的名稱空間,記錄了函式、類中定義的所有變數。
一個物件的屬性集合,也構成了一個名稱空間。但通常使用objname.attrname的間接方式訪問屬性,而不是直接訪問,故不將其列入名稱空間討論。(直接訪問:直接使用名字訪問的方式,如name,這種方式嘗試在名字空間中搜索名字name。間接訪問:使用形如objname.attrname的方式,即屬性引用,這種方式不會在名稱空間中搜索名字attrname,而是搜尋名字objname,再訪問其屬性。)
2.3 名稱空間的生命週期
不同型別的名稱空間有不同的生命週期:
內建名稱空間在Python直譯器啟動時建立,直譯器退出時銷燬;
全域性名稱空間在模組被直譯器讀入時建立,直譯器退出時銷燬;
區域性名稱空間,這裡要區分函式以及類定義。函式的區域性名稱空間,在函式呼叫時建立,函式返回結果或丟擲異常時被銷燬(每一個遞迴函式都擁有自己的名稱空間);類定義的名稱空間,在直譯器讀到類定義(class關鍵字)時建立,類定義結束後銷燬。(*)
3 作用域
3.1 什麼是作用域
作用域是針對名稱空間而言,指名稱空間在程式裡的可應用範圍,或者說是Python程式(文字)的某一段或某幾段,在這些地方,某個名稱空間中的名字可以被直接引用。這部分程式就是這個名稱空間的作用域。只有函式、類、模組會產生新的作用域,程式碼塊(例如
另外,python中變數的作用域是由它在原始碼中的位置決定的(*)。由一個賦值語句引進的名字在這個賦值語句所在的作用域裡是可見(起作用)的,而且在其內部巢狀的每個作用域內也可見,除非它被巢狀於內部的且引進同樣名字的賦值語句所遮蔽。
3.2 名稱空間的查詢順序
上述作用域的定義中表名了名稱空間與作用於之間的關係:作用於是名稱空間的可見範圍。那麼,在程式中訪問某個名稱時,是怎樣一個搜尋順序呢?按照LEGB順序搜尋:
Local:首先搜尋,包含區域性名字的最內層(innermost)作用域,如函式/方法/類的內部區域性作用域;
Enclosing:根據巢狀層次從內到外搜尋,包含非區域性(nonlocal)非全域性(nonglobal)名字的任意封閉函式的作用域。如兩個巢狀的函式,內層函式的作用域是區域性作用域,外層函式作用域就是內層函式的 Enclosing作用域;
Global:倒數第二次被搜尋,包含當前模組全域性名字的作用域;
Built-in:最後被搜尋,包含內建名字的最外層作用域。
Python按照以上LEGB的順序依次在四個作用域搜尋名字,沒有搜尋到時,Python丟擲NameError異常。所以:
在區域性作用域中,可以看到區域性作用域、巢狀作用域、全域性作用域、內建作用域中所有定義的變數。
在全域性作用域中,可以看到全域性作用域、內建作用域中的所有定義的變數,無法看到區域性作用域中的變數。
在Python中,類定義所引入的作用域對於成員函式是不可見的,這與C++或者Java是很不同的,因此在Python中,成員函式想要引用類體定義的變數,必須通過self或者類名來引用它。(我的理解是Python類中所有變數有一個作用域,每個成員函式都有各自都作用域,這些作用域都是Local,且是平級的*)
用一個類比來理解名稱空間與作用域: 四種作用域相當於我們生活中的國家(Built-in)、省(Global)、市(Enclosing)、縣(Local),名稱空間相當於公務員花名冊,記錄著哪個職位是哪個人。國家級公務員服務於全國
民眾(全國老百姓都可以喊他辦事),省級公務員只服務於本身民眾(國家層面的人或者其他省的人我不管),市(Enclosing)、縣(Local)也是一個道理。當我們要找某一類領導(例如想找
個警察幫我打架)時(要訪問某個名稱),如果我是在縣(Local)裡頭,優先在縣裡的領導花名冊中找(優先在自己作用域的名稱空間中找),縣裡花名冊中沒警察沒有就去市裡的花名冊找(往
上一層作用域名稱空間找),知道找到國家級都還沒找到,那就會報錯。如果省級民眾想找個警察幫忙大家,不會找市裡或者縣裡的,只會找自己省裡的(其它省都不行),或者找國家級的。國家、
省、市、縣肯定一直都在那裡,可不會移動(作用域是靜態的);領導可以換屆,任期移到就換人(名稱空間是動態的,每次呼叫函式都會新的名稱空間,函式執行結束,名稱空間銷燬)。
3.3 glocal與nonlocal
當在一個函式內部為一個變數賦值時,並不是按照上面所說LEGB規則來首先找到變數,之後為該變數賦值。在Python中,在函式中為一個變數賦值時,有下面這樣一條規則:
“當在函式中給一個變數名賦值是(而不是在一個表示式中對其進行引用),Python總是建立或改變本地作用域的變數名,除非它已經在那個函式中被宣告為全域性變數. ”
那麼,若想要在函式中修改全域性變數,而不是在函式中新建一個變數,此時便要用到關鍵字global
了。
i = 1 def func(): global i print(i) #輸出1 i = 2 func() print(i) #輸出2
關鍵字nonlocal的作用與關鍵字global類似,使用nonlocal關鍵字可以在一個巢狀的函式中修改巢狀作用域中的變數,示例如下:
def f1(): i = 1 def f2(): nonlocal i print(i) #輸出1 i = 2 f2() print(i) f1() #輸出2
第一,兩者的功能不同。global關鍵字修飾變數後標識該變數是全域性變數,對該變數進行修改就是修改全域性變數,而nonlocal關鍵字修飾變數後標識該變數是上一級函式中的區域性變數,如果上一級函式中不存在該區域性變數,nonlocal位置會發生錯誤(最上層的函式使用nonlocal修飾變數必定會報錯)。
第二,兩者使用的範圍不同。global關鍵字可以用在任何地方,包括最上層函式中和巢狀函式中,即使之前未定義該變數,global修飾後也可以直接使用,而nonlocal關鍵字只能用於巢狀函式中,並且外層函式中定義了相應的區域性變數,否則會發生錯誤。
對上面程式碼略作修改:
i = 0 def f1(): i = 1 def f2(): global i #此處改為glocal print(i) #輸出0 i = 2 f2() print(i) f1() #輸出2
3.4 globals()和locals()函式
根據呼叫地方的不同,globals()和locals()函式可被用來返回全域性和區域性名稱空間裡的名字。
如果在函式內部呼叫locals(),返回的是所有能在該函式裡訪問的命名。
如果在函式內部呼叫globals(),返回的是所有在該函式裡能訪問的全域性名字。
兩個函式的返回型別都是字典。所以名字們能用keys()函式摘取。
4 易錯情況
上文介紹了變數名的搜尋順序是LEGB的,其中G、B兩個作用域的引入在不能夠通過程式碼操作的,能夠通過語句引入的作用域只有E和L。Python中也只能函式���類的定義能引入新作用域。另外,在實際開發中,一定要主要函式定義引入local作用域或者Enclosing作用域中對應名稱空間的宣告週期。下面列舉Python中的幾例特殊情況。如果你覺得已經理解並掌握了上面名稱空間與作用於的知識,請嘗試解釋下面的情況:
(1)情況1:
def test(): i = 0 test() print(i)
推測出輸出結果了嗎?沒錯,會報錯:NameError: name 'i' is not defined。切記:函式的名稱空間在函式被呼叫時建立,函式執行完畢,命名就也被銷燬。另外,LEGB搜尋法則也不會讓全域性作用域去區域性作用域尋找。
(2)情況2:
if True: i = 1 print(i) # 可以正常輸出i的值1,不會報錯
if條件判斷語句不會引入新的作用域,所以,語句“i=1”與“print(i)”屬於同一作用域,既然同屬於一個作用域,也不存在說if程式碼塊執行完之後,作用域銷燬,所以i一直存在,可以正常執行。
(3)情況3:
for i in range(10): pass print(i) #輸出結果是9,而不是NameError
for迴圈不會引入新的作用域,所以,迴圈結束後,繼續執行print(i),可以正常輸出i,原理上與情況3中的if相似。這一點Python就比較坑了,因此寫程式碼時切忌for迴圈名字要與其他名字不重名才行。
(4)情況4
list_1 = [i for i in range(5)] print(i)
情況3中說到過,for迴圈不會引入新的朱用於,那麼為什麼輸出報錯呢?真相只有一個:列表生成式會引入新的作用域,for迴圈是在Local作用域裡面的。事實上,lambda、生成器表示式、列表解析式也是函式,都會引入新作用域。
(5)情況5:
def import_sys(): import sys import_sys() print(sys.path) # 報錯:NameError: name 'sys' is not defined
在函式內部進行模組匯入時,匯入的模組只在函式內部作用域生效。這個算非正常程式設計師的寫法了,import語句在函式import_sys中將名字sys和對應模組繫結,那sys這個名字還是定義在區域性作用域,跟上面的例子沒有任務區別。要時刻切記Python的名字,物件,這個其他程式語言不一樣。
(6)情況6:
只引用上層作用域中的值時:
def test(): print(i)# 可正常輸出0 i = 0 test()
在區域性作用域中可以引用全域性作用域中的名稱空間。
注:可不要認為i=0這行必須解除安裝def test()前面,事實上只需要在test()函式呼叫前寫i=0即可,因為函式的名稱空間是在函式被呼叫時建立的。
繼續上面的例子,若是對值進行修改:
def test(): print(i) i= 2 i = 0 test()
報錯:UnboundLocalError: local variable 'i' referenced before assignment
Python對區域性作用域情有獨鍾,直譯器執行到print(i),i在區域性作用域沒有。直譯器嘗試繼續執行後面定義了名字i,直譯器就認為程式碼在定義之前就是用了名字,所以丟擲了這個異常。如果直譯器解釋完整個函式都沒有找到名字i,那就會沿著搜尋鏈LEGB往上找了,最後找不到丟擲NameError異常。
是不是覺得另有所悟,對上面的程式碼稍作修改,能否推測出結果:
def test(): i = [2 , 2] i = [1 , 2] test() print(i) 輸出結果: [1 , 2]
我想你應該猜到了結果,這個和上面的例子基本是一樣的。再改一下:
def test(): i[0] = 2 i = [1 , 2] test() print(i)
輸出結果:
[2, 2]
猜到了嗎?是不是有些懵逼。list作為一個可變物件,l[0] = 2並不是對名字l的重繫結,而是對l的第一個元素的重繫結,所以沒有新的名字被定義。因此在函式中成功更新了全域性作用於中l所引用物件的值。
(7)情況7:
請對比下面幾種示例程式碼:
第一種:
i = 1 def f1(): print(i) def f2(): i = 2 f1() f2() print(i)
第二種:
i = 1 def f1(): print(i) def f2(): i = 2 return f1 ret = f2() ret() print(i)
第三種:
i = 1 def f1(): i = 2 def f2(): print(i) return f2 func = f1() func() print(i)
先別看答案,想想輸出結果!
第一種輸出結果:
1
1
第二種輸出結果:
1
1
第三種輸出結果:
2
1
為什麼會這樣呢?上面說到過,函式的作用域是靜態的,由函式宣告的位置決定,在哪裡宣告,就決定了它的上層作用域是誰,這與呼叫函式的位置無關。無論在哪裡呼叫,它都會去函式本身的作用域中的名稱空間找,找不到在去上一層的名稱空間找,切記未必是在呼叫該函式的作用域的名稱空間找。對於第三種情況,是最讓我費解的地方,func = f1()執行完之後,f1的名稱空間被銷燬,按理說就找不到i=2了,但是輸出結果確實是2,所以我只能用LEGB搜尋法則解釋。(如果你知道為什麼,請給我留言,感激不盡……)
(8)情況8:
class A(object): a = 2 def fun(self): print(a) new_class = A() new_class.fun()
程式碼執行後報錯:NameError: name 'a' is not defined。上文中說過,Python類成員變數與成員函式都有自己的作用域,且各作用域平級。(用作用域的生命週期來解釋也行,但是真心覺得不對勁)。
5 總結
Python的作用域與名稱空間有的時候真的讓人很費解,我本以為與Java等語言類似的,沒想多還是挺有區別的。有些情況我到現在也沒想通,例如作用域與名稱空間的生命週期,用生命週期來解釋上面的一些例子,總覺得不對勁。