1 引言
命名空間與作用域是程序設計中的基礎概念,深入理解有助於理解變量的生命周期,減少代碼中的莫名其妙bug。Python的命名空間與作用域與Java、C++等語言有很大差異,若不注意,就可能出現莫名其妙的問題。
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程序(文本)的某一段或某幾段,在這些地方,某個命名空間中的名字可以被直接引用。這部分程序就是這個命名空間的作用域。只有函數、類、模塊會產生新的作用域,代碼塊(例如if、for代碼塊)不會產生新的作用域。
另外,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等語言類似的,沒想多還是挺有區別的。有些情況我到現在也沒想通,例如作用域與命名空間的生命周期,用生命周期來解釋上面的一些例子,總覺得不對勁。期間翻閱了n多前輩的博客資料,到各有說法,或許是我沒理解到位,若有前輩看到這里,又剛好知道原因,請為晚輩留言解惑,感激不盡!
參考資料:
https://www.jb51.net/article/114951.htm
http://python.jobbole.com/86465/
http://python.jobbole.com/81367/?utm_source=blog.jobbole.com&utm_medium=relatedPosts