1. 程式人生 > >Python中名稱空間與作用域使用總結

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程式(文字)的某一段或某幾段,在這些地方,某個名稱空間中的名字可以被直接引用。這部分程式就是這個名稱空間的作用域。只有函式、類、模組會產生新的作用域,程式碼塊(例如

iffor程式碼塊)不會產生新的作用域。

  另外,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等語言類似的,沒想多還是挺有區別的。有些情況我到現在也沒想通,例如作用域與名稱空間的生命週期,用生命週期來解釋上面的一些例子,總覺得不對勁。