Python 作用域(scope) 和 LEGB

發表於2017-01-18

約束 名字空間 作用域 之間的那些事

不管在什麼程式語言, 都有作用域這個概念.作用域控制在它範圍內程式碼的生存週期, 包括名字和實體的繫結.

名字和實體的繫結, 我們可以理解成賦值. num = int_obj, 當我們執行這句程式碼時, 實際上我們已經得到一個(‘num’, int_obj)的關聯關係, 我們也能將稱之為約束, 這個約束也將存在名字空間(name space)裡面, 名字空間也將是LEGB查詢的依據.

而每個名字空間, 也將對應一個作用域, 作用域是程式碼正文中的一段程式碼區域, 作用域的有效範圍更多是這段程式碼區域去衡量,一個作用域可以有多個名字空間, 一個名字空間也能有多個約束(多個賦值語句)

可以通過sys._getframe().f_code.co_name 檢視程式碼所處的作用域, 先來看下sys._getframe是什麼鬼吧?

從函式的定義可以看到, sys._getframe將返回一個frameobject物件, 那其實frameobject是什麼物件? 為什麼它能決定作用域?

frameobjec實際上就是python虛擬機器上所維護的每個棧幀, 這和我們常規理解的棧幀多點差別, 因為python在原有棧幀的基礎上, 在封裝一層形成自己的棧幀. 雖然是有些不同, 但是我們還是能近似看成常規理解的棧幀, 包括入棧,出棧 區域性變數等等

那麼frameobejct裡面究竟有什麼?

我們現在已經知道frameobject的來歷呢, 那麼再回顧上面提到的: sys._getframe().f_code.co_name

毫無疑問, 我們還是得看下codeobject是什麼東西, 才能知道name的意思:

同樣也是print help大法

雖然 sys._getframe().f_code.co_name 頂多也只能說明, 這段程式碼是在哪個code block裡面, 並沒有直接證明就是作用域, 但是從上面也已經談到, 作用域是從程式碼正文的程式碼片段的決定, So, 也能近似看成算是作用域的名字了~

作用域話題似乎聊得有點深入了, 讓我們暫告一段落, 繼續講講 約束 和 作用域的關係吧

每個約束一旦建立, 將會持續的影響後面程式碼的執行, 但是約束也只能在名字空間內生效, 也就是說,一旦出了名字空間/作用域. 約束也將失效

在上面例子可以看到, 變數a在模組層和函式f層都有賦值, 在執行函式f時,輸出6, 但是在下面卻輸出了3, 也就是因為函式f 中的 a=3 約束只有在函式f的作用域中生效,函式結束,a的值, 應該是最開始的a=3來控制, 我們現在應該隱約有種感覺, 為什麼賦值語句會被稱為約束? 我們完全可以理解成, 一個變數名, 可能有多次改變其繫結的實體物件的機會, 但是最終顯示是哪個實體, 完全就是從作用域->名字空間->約束 來決定

 

LEGB

從上面我們已經清楚 約束,名字空間, 作用域之間微妙的關係, 那麼我們接下來就應該探討下變數查詢的方式了.

LEGB 分別是:

  • locals 是函式內的名字空間,包括區域性變數和形參
  • enclosing 外部巢狀函式的名字空間(閉包中常見)
  • globals 全域性變數,函式定義所在模組的名字空間
  • builtins 內建模組的名字空間

而查詢的優先順序從左到右以此是: L -> E -> G -> B

從上面我們已經知道, 約束, 是受作用域和名字空間的影響, 所以查詢肯定也是隻能在名字空間去進行

來些簡單程式碼吧:

這段相信大家都知道為什麼能夠輸出3, 當在函式內部的名字空間找不到關於變數a的約束時, 將會去全域性變數的名字空間查到, OK, 已經找到了 (a,3)的約束, 返回 3., test()也是同理

同樣的, 在函式內部和模組內部都不能找到open的約束, 那麼只能去Bulitin(內建名字空間)去查詢了, 找到了open了, 並且還是個函式, 所以返回 <built-in function open>

簡單的演示完, 來些神奇的程式碼:

有沒有覺得很奇怪, a=4是在函式f裡面定義的, 但是返回v的時候, 函式已經退出,理應釋放了, 為什麼test()還能輸出4呢? 其實原因很簡單, 首先這個已經是閉包函式了, 同樣的還是遵循LEGB的原則, 函式v已經能夠在外層巢狀作用域找到a的定義, 又因為閉包函式有個特點, 在構建的時候, 能夠將需要的約束也一併繫結到自身裡頭, 所以即使函式f退出了, 變數a釋放了, 但是不要緊, 函式v已經繫結好了相應的約束了, 自然而然也就能輸出4。

相關文章