Python作用域和名稱空間

veelion發表於2019-03-09

名稱空間和作用域的概念我們之前也提到過,比如內建函式globals(),函式中變數的作用域,模組使用的import等等。這些可能讓我們對這兩個概念有了大致的理解。本節再詳細探討一下。

名稱空間和作用域

Python名稱空間

名稱空間,就是一個從名稱到物件的對映關係。

對於這個概念的理解,我們打個比方:河西村有個人(物件)叫張三(名稱),河東村有個人(物件)也叫張三(名稱),倆人雖然都叫張三(名稱),但是他們倆不是同一個人(物件),因為他們屬於不同的村(名稱空間)。有一天,河西村的張三名聲大了,傳播到鎮上了(名稱被import到“鎮”這個空間),鎮上的人講起“張三”時,就是說的河西村的,要是說河東村的張三,就要特別說“河東村的張三”(河東村.張三)。這就是名稱空間的意思——對映了名稱到物件的名稱範圍。

目前,大部分的名稱空間都是由Python的字典實現的,通常我們不會去關注它們,處理要面對效能問題時,並且這種實現可能在將來改變。所以說,我們不需要深究名稱空間的內部實現,但要搞明白它的使用。

下面是幾個名稱空間的例子:

  • 內建函式的集合(包含print()等內建函式和內建異常等);
  • 模組中的全域性名稱;
  • 函式呼叫中的本地名稱。

另外,從某種含義上說,物件的屬性集合也是一種名稱空間的形式。正如我們前面舉的張三的例子那樣,不同名稱空間中的名稱之間沒有任何關係。比如,兩個不同模組都可以定義函式max()而不會產生混淆,模組的使用者要呼叫某個max()函式就要在其前面加上模組名稱。(詳見import的使用)

Python屬性

我們把任何跟在一個點號之後的名稱都稱為屬性。例如,在表示式a.name中,real是物件a的一個屬性。同樣對模組中函式的引用也是屬性引用,在表示式modname.funcname中,modname是一個模組物件,而funcname是它的一個屬性。

屬性可以是隻讀的也可以是可寫的。如果是可寫的,那麼我們就可以對屬性進行賦值,比如,modname.name = '猿人學Python'。可寫的屬性同樣可以用del語句刪除,比如del modname.name將把modname物件的name屬性移除。

不同時刻建立的名稱空間有不同的生存期:

  • 包含內建名稱的名稱空間是在Python直譯器啟動時建立的,永遠不會被刪除(除非退出直譯器);
  • 模組的全域性名稱空間在模組定義被讀入(import)時建立,通常,模組名稱空間也會持續到直譯器退出;
  • 從指令碼檔案(.py.pyc)讀取或互動式(直譯器shell)讀取而被直譯器的頂層呼叫執行的語句,被認為是__main__模組呼叫的一部分,它們有自己的全域性名稱空間;
  • 函式的本地名稱空間建立於該函式被呼叫的時刻,並且在函式返回或丟擲一個不在函式內部處理的異常時被刪除。遞迴函式的每次遞迴呼叫都會建立它自己的本地名稱空間;

內建名稱實際上也存在於一個模組中,它叫做builtins

Python作用域

作用域,是一個名稱空間可直接發放完的Python程式碼的文字區域。這裡的“可直接訪問”的意思是,對名稱的不加點號(非限定性)引用會嘗試在名稱空間中查詢該名稱。

儘管作用域是靜態確定的,但它們是動態使用的。在執行期間的任何時刻,至少有三個巢狀的作用域,它們的名稱空間可以直接訪問:

  • 最內部作用域:最先搜尋該作用域,包含區域性名稱
  • 封閉函式作用域:從最近的封閉作用域開始搜尋,包含非區域性名稱,也包括非全域性名稱
  • 倒數第二個作用域:包含當前模組的全域性名稱
  • 最外面的作用域:最後搜尋,是包含內建名稱的名稱空間

如果一個名稱被宣告為全域性變數,則所有引用和賦值將直接指向包含該模組的全域性名稱的中間作用域。 要重新繫結在最內層作用域以外找到的變數,可以使用nonlocal語句宣告為非本地變數。 如果沒有被宣告為非本地變數,這些變數將是隻讀的(嘗試寫入這樣的變數只會在最內層作用域中建立一個新的區域性變數,而同名的外部變數保持不變)。

很重要的一點:作用域是按文字方式確定的,模組內定義的函式的全域性作用域就是該模組的名稱空間,無論該函式從什麼地方或以什麼別名被呼叫。另一方面,實際的名稱搜尋是在執行時動態完成的。

Python 的一個特殊之處在於: 如果不存在生效的global語句,對名稱的賦值總是進入最內層作用域。 賦值不會複製資料,它們只是將名稱繫結到物件。刪除也是如此,語句del x會從區域性名稱空間的引用中移除對x的繫結。事實上,所有引入新名稱的操作都使用區域性作用域,特別是import語句和函式定義會在區域性作用域中繫結模組或函式名稱。

global語句可被用來表明特定變數生存於全域性作用域並且應當在其中被重新繫結;nonlocal語句表明特定變數生存於外層作用域中並且應當在其中被重新繫結。

下面我們來看一個作用域和名稱空間的例子,它演示流量如何引用不同作用域和名稱空間以及globalnonlocal如何影響變數繫結:

def scope_demo():
    def do_local():
        name = 'local name'

    def do_nonlocal():
        nonlocal name
        name = 'nonlocal name'

    def do_global():
        global name
        name = 'global name'

    name = 'demo name'
    do_local()
    print('After local assignment:', name)
    do_nonlocal()
    print('After nonlocal assignment:', name)
    do_global()
    print('After global assignment:', name)

scope_demo()
print('In global scope:', name)

思考一下,上面的程式碼會輸出怎樣的結果?如果你對上面的講解理解透了,你的思考結果應該是這樣的:

After local assignment: demo name
After nonlocal assignment: nonlocal name
After global assignment: nonlocal name
In global scope: global name

這裡要說明的是,do_global()函式修改了全域性變數name,並沒有對scope_demo()函式的區域性變數name做修改,所以列印了After global assignment: nonlocal name

區域性賦值(預設情況)不會改變scope_demoname的繫結;nonlocal賦值會改變函式scope_demoname的繫結,而global賦值會改變模組層級的繫結(不是scope_demo內部的name,而是其之外的全域性作用域下的name)。

命令空間和作用域總結:

名稱空間定義了一個名稱的範圍,作用域指定了能看到名稱空間的文字區域(程式碼)。程式碼執行時,名稱搜尋的順序和範圍如下:

  • 最內部作用域:最先搜尋該作用域,包含區域性名稱
  • 封閉函式作用域:從最近的封閉作用域開始搜尋,包含非區域性名稱,也包括非全域性名稱
  • 倒數第二個作用域:包含當前模組的全域性名稱
  • 最外面的作用域:最後搜尋,是包含內建名稱的名稱空間

相關練習題

參照scope_demo(),練習區域性賦值、nonlocal賦值、global賦值。

猿人學banner宣傳圖

我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。

***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***

相關文章