Python 的名稱空間

發表於2016-03-08

懶得掃全文的童鞋,可以直接跳到最後看總結。
我們先從一個簡單的栗子說起:

栗子

a 檔案中有變數 va 以及類 A,b 檔案匯入 aclass A ,並列印出 A

執行 b 檔案的結果為:

可以發現,雖然 b 只是匯入了 a 中的 class A,但匯入這個過程卻執行了整個 a 檔案,那麼我們是否能夠在 b 中訪問 a 中的全域性變數 va 呢:

嘗試了各類呼叫方法,發現都無法正常訪問 a 的全域性變數 va,既然 b 的匯入執行了整個 a 檔案,甚至還列印出了 vaid 和值,又為什麼無法在 b 中呼叫 va 呢?

這個問題所涉及到的內容就是:名稱空間。

但在開始正題之前,我們需要闡明若干概念:

一些基本概念的澄清

物件

Python 一切皆物件,每個物件都具有 一個ID、一個型別、一個值;物件一旦建立,ID 便不會改變,可以直觀的認為 ID 就是物件在記憶體中的地址

上例 a, b 共享了同一個 ID、同一個值、同一個型別。因此 a, b 表達的是同一個物件,但 a, b 又明顯是不同的,比如一個叫 'a' 一個叫 'b'…既然是同一個物件,為什麼又有不同的名字呢?難道名字不是物件的屬性?

識別符號

事實確實如此,這是 Python 比較特殊一點:如同'a' 'b' 這樣的名稱其實有一個共同的名字:identifier(注意不要與 ID 混淆了),中文名為“識別符號”,來解釋一下:

識別符號:各類物件的名稱,比如函式名、方法名、類名,變數名、常量名等。

在 Python 中賦值並不會直接複製資料,而只是將名稱繫結到物件,物件本身是不知道也不需要關心(該關心這個的是程式猿)自己叫什麼名字的。一個物件甚至可以指向不同的識別符號,上例中的'a' 'b'便是如此。真正管理這些名子的事物就是本文的主角”名稱空間” 。

名稱空間

名稱空間:(英語:Namespace)表示識別符號(identifier)的可見範圍。(ps:copy 自 SF)

簡而言之,名稱空間可以被簡單的理解為:存放和使用物件名字的抽象空間

作用域

與名稱空間相對的一個概念就是“作用域”,那麼什麼又是作用域呢?

作用域:(英文 Scope)是可以直接訪問到名稱空間的文字區域。

這裡需要搞清楚什麼是直接訪問:

Python 中不加 . 的訪問為直接訪問,反之為屬性訪問。
因此作用域必定是相對某個物件內部而言的,比如一個函式內部、一個模組全域性等,那作用域和名稱空間是什麼關係呢:

  1. 作用域是一種特殊的名稱空間,該空間內的名稱可以被直接訪問;
  2. 並不是所有的名稱空間都是作用域。

看不懂? 沒關係,後面會解釋,我們先回到名稱空間這個話題上:

現今 Python 的大部分名稱空間是通過字典來實現的,也即一個名稱空間就是名字到物件的對映。另外, Python 允許對名稱空間進行巢狀,而我們經常接觸的名稱空間有四層:

LEGB 規則

LEGB 層級

這四層名稱空間可以簡記為 LEGB:

  1. 區域性名稱空間(local):指的是一個函式所定義的空間或者一個類的所有屬性所在的空間,但需注意的是函式的區域性名稱空間是一個作用域,而類的區域性名稱空間不是作用域。
  2. 閉包名稱空間(enclosing function):閉包函式 的作用域(Python 3 引入)。
  3. 全域性名稱空間(global):讀入一個模組(也即一個.py文件)後產生的作用域。
  4. 內建名稱空間(builtin):Python 直譯器啟動時自動載入__built__模組後所形成的名字空間;諸如 str/list/dict…等內建物件的名稱就處於這裡。

為了說清楚這幾層洋蔥皮,舉個例子:

內建名稱空間比較好理解,我們重點講解下其他三個:

  1. 'v1' 為全域性變數 v1 的名子,其所處的名稱空間為全域性名稱空間;需要注意的是全域性名稱空間包括 'func' 但不包括 func 的作用域。
  2. func 內部囊括 'v2''inn_func' 名稱的空間為區域性名稱空間;
  3. 執行 func 後,func 的作用域釋放(或許遺忘更合適),並返回了繫結了 vv2 變數的閉包函式 inn_func,此時閉包所具有的作用域為閉包空間,因此區域性名稱空間和閉包名稱空間是相對而言的,對於父函式 func 而言,兩者具有產生時間上的差異。

LEGB 訪問規則

搞清楚各個層級概念後,我們來說一下 LEGB 的訪問規則: 同樣的識別符號在各層名稱空間中可以被重複使用而不會發生衝突,但 Python 尋找一個識別符號的過程總是從當前層開始逐層往上找,直到首次找到這個識別符號為止

上例中,全域性變數和函式 f 都定義了 變數 v1,結果 Python 會優先選擇 f 的區域性變數 v1 ,對於 f 內並未定義的變數 v2 ,Python 會向上搜尋全域性名稱空間,讀取全域性變數 v2 後列印輸出。

global 和 nonlocal 語句

global 和 nonlocal 的作用

如前所述,對於上層變數,python 允許直接讀取,但是卻不可以在內層作用域直接改寫上層變數,來看一個典型的閉包結構:

上面對函式內的 gvlv 進行賦值操作後,兩處都會發生 UnboundLocalError,因為 Python 並不知道你是想在內層作用域生成一個同名的區域性變數,還是想直接改寫上層變數,因此便會引起錯誤。為此,Python 引入了 globalnonlocal 語句就來說明所修飾的 gvlv 分別來自全域性作用域和父函式作用域,宣告之後,就可以在 funcinn_func 內直接改寫上層作用域內 gvlv 的值:

如上,全域性變數 gv 值被函式改寫了, inn_func 修改的也確實是父函式 lv的值 (依據 ID 判斷)。

借殼

那麼是不是不使用 globalnonlocal 就不能達到上面的目的呢?來看看這段程式:

執行的結果:

可以發現,執行結果同上面完全一致,問題自然來了:“為什麼不用 global nonlocal 也可以改寫全域性變數gv和父函式變數lv的值?

為了看清楚這個過程,我們將上面的gv.insert(0, 'gv') lv.append(v) 改寫為 gv[0:0] = ['gv'] lv[:] = [v]:

執行結果:

同 g.py 檔案的執行結果完全一致,事實上兩者之間的內在也是完全一樣的。
So 我們其實改寫的不是 gvlv ,而是 gvlv 的元素 gv[0:0]lv[:] 。因此,不需要 globalnonlocal 修飾就可以直接改寫,這就是“借殼”,nonlocal 尚未引入 Python 中,比如 Python 2.x 若要在子函式中改寫父函式變數的值就得通過這種方法。
當然借殼蘊藏著一個相對複雜的識別符號建立的問題:比如子函式通過借殼修改父函式變數lv的值,那麼子函式的識別符號lv是怎麼繫結到父函式變數lv的值 ID 的上的?

關於這個問題,這裡有個問答就是討論這個的:python的巢狀函式中區域性作用域問題?

global 和 nonlocal 語句對識別符號建立的不同影響

另外,需要注意的是:global 語句只是宣告該識別符號引用的變數來自於全域性變數,但並不能直接在當前層建立該識別符號;nonlocal 語句則會在子函式名稱空間中建立與父函式變數同名的識別符號:

執行結果:

之所以 nonlocal 語句與 global 語句的處置不同,在於全域性變數在模組內隨時都可以訪問,而父函式變數在父函式執行完畢後便會釋放,因此 nonlocal 語句必須將父函式變數的識別符號和引用寫入閉包名稱空間。

名稱空間和識別符號的建立

建立規則

實際上,到這裡其實還有一個重要的重要問題沒有解決:“識別符號並不是天生就在名稱空間內的,名稱空間也不是平白無故就產生的,那麼什麼時候會建立名稱空間和識別符號呢?”
規則有三:

  1. 賦值、定義函式和類時產生識別符號;
  2. 類和函式定義(def 和 lambda)執行時產生新的名稱空間;
  3. 識別符號產生地點決定識別符號所處的名稱空間。

這三點就是拿來秒懂的!不過,仍然有一點常常被忽視:類的名稱空間:

類的區域性名稱空間

首先,函式和類執行時都會產生區域性名稱空間,但類的執行機制不同於函式:

執行檔案,結果為:

如上,類就是一個可執行的程式碼塊,只要該類被載入,就會被執行,這一點不同於函式。
類之所以這麼設計的原因在於:類是建立其他例項(生成其他的類或者具體的物件)的物件,因此必須在例項之前被建立,而類又可能涉及到與其他類的繼承、過載等一系列問題,故在程式碼載入時就被建立利於提高效率和降低邏輯複雜度。

其次,與函式不同的是,類的區域性名稱空間並非作用域

執行上段程式碼,我們可以發現在類 A 內列表推導式無法調取 a 的值,但函式卻可以:

因此,A 中的 a 不同於函式 func 中的 a 在區域性名稱空間中可以被任意讀取,之所以說是“不可以被任意”讀取而不是“不可被讀取”,原因在於在類A 的區域性空間內,a 其實一定程度上是可以直接被讀取的:

執行上段程式碼後:

而上例中 b 的賦值操作不能執行,原因在於列表推導式會建立自己的區域性名稱空間,因此難以訪問到 a

編譯與區域性名稱空間

Python 是動態語言,很多行為是動態發生的,但 Python 自身也在不斷進步,比如為了提高效率,有些行為會在編譯時候完成,區域性變數的建立就是如此:

上段程式還未執行,就提示存在有語法錯誤,原因在於python 直譯器發現 inn_func 記憶體在自身的 a 變數,但卻在宣告之前就被 print 了。

總結

囉嗦了這麼多,終於該結尾了!
我們再來回過頭來看下文章開頭的栗子:
1、為什麼 b.py 只是匯入 a.py 中的 class A,卻執行了整個 a.py 檔案?
答:因為 Python 並不知道 class A 在 a.py 文件的何處,為了能夠找到 class A,Python 需要執行整個文件。
2、為什麼 b.py 的匯入執行了整個 a.py 文件,卻在 b 中難以呼叫 a 的全域性變數 va
答:Python 的全域性變數指的是模組全域性,因此不可以跨文件,因此 global 語句也是不可以跨文件的。另外, b 只是匯入了 a 的 class A,因此並不會匯入 a 中所有的識別符號,所以 類似a.va 這樣的呼叫也是不起作用的。

關於名稱空間:
1、賦值、定義類和函式都會產生新的識別符號;
2、全域性變數的識別符號不能跨文件;
3、各級名稱空間相互獨立互不影響;
4、Python 總是從當前層逐漸向上尋找識別符號;
5、內層作用域若想直接修改上層變數,需要通過 global nonlocal 語句先宣告;
6、單純的 global 語句並不能為所在層級建立相應識別符號,但 nonlocal 語句可以在閉包空間中建立相應識別符號;
7、類的區域性名稱空間不是作用域。

 

相關文章