由一個例子到python的名字空間

發表於2016-10-20

例子引入

例1

可以正常輸出結果: 並且需要注意,在func2使用x變數之前的名字空間就已經有了'x':1.

稍微改一點:如下

例2:

輸出就開始報錯: 而且在before func2也沒有了x.

這兩個例子正好涉及到了python裡面最核心的內容:名字空間,正好總結一下,然後在解釋這幾個例子。


名字空間(Namespace)

比如我們定義一個”變數”

所以,這裡更準確的叫法應該是名字。 一些語言中比如c,c++,java 變數名是記憶體地址別名, 而Python 的名字就是一個字串,它與所指向的目標物件關聯構成名字空間裡面的一個鍵值對{name: object},因此可以這麼說,python的名字空間就是一個字典.。

分類

python裡面有很多名字空間,每個地方都有自己的名字空間,互不干擾,不同空間中的兩個相同名字的變數之間沒有任何聯絡一般有4種:LEGB四種

  • locals: 函式內部的名字空間,一般包括函式的區域性變數以及形式引數
  • enclosing function: 在巢狀函式中外部函式的名字空間, 對fun2來說, fun1的名字空間就是。
  • globals: 當前的模組空間,模組就是一些py檔案。也就是說,globals()類似全域性變數。
  • __builtins__: 內建模組空間, 也就是內建變數或者內建函式的名字空間。

當程式引用某個變數的名字時,就會從當前名字空間開始搜尋。搜尋順序規則便是: LEGB.

一層一層的查詢,找到了之後,便停止搜尋,如果最後沒有找到,則丟擲在NameError的異常。這裡暫時先不討論賦值操作。
比如例1中的a = x + 1 這行程式碼,需要引用x, 則按照LEGB的順序查詢,locals()也就是func2的名字空間沒有,進而開始E,也就是func1,裡面有,找到了,停止搜尋,還有後續工作,就是把x也加到自己的名字空間,這也是為什麼fun2的名字空間裡面也有x的原因。

訪問方式

其實上面都已經說了,這裡暫時簡單列一下

  1. 使用locals()訪問區域性名稱空間
  2. 使用globals()訪問全域性名稱空間
    這裡有一點需要注意,就是涉及到了from A import B 和import A的一點區別。

輸出結果:

從輸出結果可以看出globals()包含了定義的函式,變數等。對於'deepcopy': <function deepcopy at 0x7f1c3d6177d0>可以看出deepcopy已經被匯入到自己的名字空間了,而不是在copy裡面。 而匯入的import copy則還保留著自身的名字空間。因此要訪問copy的方法,就需要使用copy.function了。這也就是為什麼推薦使用import module的原因,因為from A import B這樣會把B引入自身的名字空間,容易發生覆蓋或者說汙染。

生存週期

每個名字空間都有自己的生存週期,如下:

  • __builtins__: 在python直譯器啟動的時候,便已經建立,直到退出
  • globals: 在模組定義被讀入時建立,通常也一直儲存到直譯器退出。
  • locals : 在函式呼叫時建立, 直到函式返回,或者丟擲異常之後,銷燬。 另外遞迴函式每一次均有自己的名字空間。

看著沒有問題,但是有很多地方需要考慮。比如名字空間都是在程式碼編譯時期確定的,而不是執行期間。這個也就可以解釋為什麼在例1中,before func2:的locals()裡面包含了x: 1 這一項。再看下面這個,

肯定會報錯的,但是錯誤不是

而是:

雖然x = 10永遠不會執行,但是在執行之前的編譯階段,就會把x作為locals變數,但是後面編譯到print的時候,發現沒有賦值,因此直接丟擲異常,locals()裡面便不會有x。這個就跟例子2中,before func2裡面沒有x是一個道理。

賦值

為什麼要把賦值單獨列出來呢,因為賦值操作對名字空間的影響很大,而且很多地方需要注意。
核心就是: 賦值修改的是名稱空間,而不是物件, 比如:

這個語句就是把a放入到了對應的名稱空間, 然後讓它指向一個值為10的整數物件。

這個就是把a放入到名字空間,然後指向一個列表物件, 然而後面的a.append(1)這句話只是修改了list的內容,並沒有修改它的記憶體地址。因此
並沒有涉及到修改名字空間。
賦值操作有個特點就是: 賦值操作總是在最裡層的作用域.也就說,只要編譯到了有賦值操作,就會在當前名字空間內新建立一個名字,然後開始才繫結物件。即便該名字已存在於賦值語句發生的上一層作用域中;

總結

分析例子

現在再看例子2, 就清晰多了, x += x 編譯到這裡時,發現了賦值語句,於是準備把x新加入最內層名字空間也就是func2中,即使上層函式已經存在了; 但是賦值的時候,又要用到x的值, 然後就會報錯:

這樣看起來好像就是 內部函式只可以讀取外部函式的變數,而不能做修改,其實本質還是因為賦值涉及到了新建locals()的名字。
在稍微改一點:

這個結果就是:

咋正確了呢—這不應該要報錯嗎? 其實不然,就跟上面的a.append(1)是一個道理。
x[0] += x[0] 這個並不是對x的賦值操作。按照LEGB原則, 搜到func1有變數x並且是個list, 然後將其加入到自己的locals(), 後面的x[0] += x[0], 就開始讀取x的元素,並沒有影響func2的名字空間。另外無論func1func2的名字空間的x 沒有什麼關係,只不過都是對[1, 2]這個列表物件的一個引用。
這個例子其實也給了我們一個啟發,我們知道內部函式無法直接修改外部函式的變數值,如例2,如果藉助list的話, 就可以了吧!比如把想要修改的變數塞到一個list裡面,然後在內部函式裡面做改變!當然python3.x裡面有了nonlocal關鍵字,直接宣告一下就可以修改了。看到這裡,對作用域理解應該有一點點了吧。

延伸

與閉包的不同

我們都知道閉包是把外部函式的值放到func.func_closure裡面,為什麼不像上面的例子一樣直接放到函式的名字空間呢?
這是因為locals()空間是在函式呼叫的時候才建立! 而閉包只是返回了一個函式, 並沒有呼叫,也就沒有所謂的空間。

locals()與globals()

在最外層的模組空間裡locals()就是globals()

另外我們可以手動修改globals()來建立名字

但是locals()在函式裡面的話, 貌似是不起作用的,如下:

這是因為直譯器會將 locals 名字複製到 一個叫FAST的 區域來優化訪問速度,而實際上直譯器訪問物件時,是從FAST區域裡面讀取的,而非locals()。所以直接修改locals()並不能影響x(可以使用exec 動態訪問,不再細述)。 不過賦值操作,會同時重新整理locals()FAST區域。


查了不少資料,說了這麼多,我只想說,作為python最核心的東西,名字空間還有很多很多地方需要探究,比如

  • 作用域(scope)與名字空間, 這裡只是模糊了二者的區別
  • 物件導向,也就是類的名字空間, 又有不一樣的地方。。。

學一點記錄一點吧。

相關文章