約束 名字空間 作用域 之間的那些事
不管在什麼程式語言, 都有作用域這個概念.作用域控制在它範圍內程式碼的生存週期, 包括名字和實體的繫結.
名字和實體的繫結, 我們可以理解成賦值. num = int_obj, 當我們執行這句程式碼時, 實際上我們已經得到一個(‘num’, int_obj)的關聯關係, 我們也能將稱之為約束, 這個約束也將存在名字空間(name space)裡面, 名字空間也將是LEGB查詢的依據.
而每個名字空間, 也將對應一個作用域, 作用域是程式碼正文中的一段程式碼區域, 作用域的有效範圍更多是這段程式碼區域去衡量,一個作用域可以有多個名字空間, 一個名字空間也能有多個約束(多個賦值語句)
可以通過sys._getframe().f_code.co_name 檢視程式碼所處的作用域, 先來看下sys._getframe是什麼鬼吧?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# sys module def _getframe(depth=None): # real signature unknown; restored from __doc__ """ _getframe([depth]) -> frameobject Return a frame object from the call stack. If optional integer depth is given, return the frame object that many calls below the top of the stack. If that is deeper than the call stack, ValueError is raised. The default for depth is zero, returning the frame at the top of the call stack. This function should be used for internal and specialized purposes only. """ pass |
從函式的定義可以看到, sys._getframe將返回一個frameobject物件, 那其實frameobject是什麼物件? 為什麼它能決定作用域?
frameobjec實際上就是python虛擬機器上所維護的每個棧幀, 這和我們常規理解的棧幀多點差別, 因為python在原有棧幀的基礎上, 在封裝一層形成自己的棧幀. 雖然是有些不同, 但是我們還是能近似看成常規理解的棧幀, 包括入棧,出棧 區域性變數等等
那麼frameobejct裡面究竟有什麼?
1 2 3 4 5 6 7 8 9 10 11 |
# help(sys._getframe()) # Output: class frame(object) ..... # 省略 | Data descriptors defined here: | f_back # 上一個棧幀物件(誰呼叫自己) | f_builtins # 內建名字空間 | f_locals # 全域性名字空間 | f_globals # 全域性名字空間 | f_code # 幀指向的 codeObject物件 ..... # 省略 |
我們現在已經知道frameobject的來歷呢, 那麼再回顧上面提到的: sys._getframe().f_code.co_name
毫無疑問, 我們還是得看下codeobject是什麼東西, 才能知道name的意思:
同樣也是print help大法
1 2 3 4 5 6 7 8 9 10 11 |
# print help(sys._getframe().f_code) # Output: class code(object) ...... # 省略 | Data descriptors defined here: | | co_name # code block的名字, 通常是類名或者函式名 /* string (name, for reference) */ | | co_names # code block中所有的名字 /* list of strings (names used) */ | ...... # 省略 |
雖然 sys._getframe().f_code.co_name 頂多也只能說明, 這段程式碼是在哪個code block裡面, 並沒有直接證明就是作用域, 但是從上面也已經談到, 作用域是從程式碼正文的程式碼片段的決定, So, 也能近似看成算是作用域的名字了~
作用域話題似乎聊得有點深入了, 讓我們暫告一段落, 繼續講講 約束 和 作用域的關係吧
每個約束一旦建立, 將會持續的影響後面程式碼的執行, 但是約束也只能在名字空間內生效, 也就是說,一旦出了名字空間/作用域. 約束也將失效
1 2 3 4 5 6 |
a = 3 def f(): a = 6 print a # 輸出 6 f() print a # 輸出 3 |
在上面例子可以看到, 變數a在模組層和函式f層都有賦值, 在執行函式f時,輸出6, 但是在下面卻輸出了3, 也就是因為函式f 中的 a=3 約束只有在函式f的作用域中生效,函式結束,a的值, 應該是最開始的a=3來控制, 我們現在應該隱約有種感覺, 為什麼賦值語句會被稱為約束? 我們完全可以理解成, 一個變數名, 可能有多次改變其繫結的實體物件的機會, 但是最終顯示是哪個實體, 完全就是從作用域->名字空間->約束 來決定
LEGB
從上面我們已經清楚 約束,名字空間, 作用域之間微妙的關係, 那麼我們接下來就應該探討下變數查詢的方式了.
LEGB 分別是:
- locals 是函式內的名字空間,包括區域性變數和形參
- enclosing 外部巢狀函式的名字空間(閉包中常見)
- globals 全域性變數,函式定義所在模組的名字空間
- builtins 內建模組的名字空間
而查詢的優先順序從左到右以此是: L -> E -> G -> B
從上面我們已經知道, 約束, 是受作用域和名字空間的影響, 所以查詢肯定也是隻能在名字空間去進行
來些簡單程式碼吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
a = 3 def f(): print a # 輸出 3 print open # 輸出 <built-in function open> f() print '----------------------分割線----------------' a = 3 def f(): def v(): print a return v test = f() test() # 輸出 3 |
這段相信大家都知道為什麼能夠輸出3, 當在函式內部的名字空間找不到關於變數a的約束時, 將會去全域性變數的名字空間查到, OK, 已經找到了 (a,3)的約束, 返回 3., test()也是同理
同樣的, 在函式內部和模組內部都不能找到open的約束, 那麼只能去Bulitin(內建名字空間)去查詢了, 找到了open了, 並且還是個函式, 所以返回 <built-in function open>
簡單的演示完, 來些神奇的程式碼:
1 2 3 4 5 6 7 8 |
a = 3 def f(): a = 4 def v(): print a return v test = f() test() # 輸出 4 Why? |
有沒有覺得很奇怪, a=4是在函式f裡面定義的, 但是返回v的時候, 函式已經退出,理應釋放了, 為什麼test()還能輸出4呢? 其實原因很簡單, 首先這個已經是閉包函式了, 同樣的還是遵循LEGB的原則, 函式v已經能夠在外層巢狀作用域找到a的定義, 又因為閉包函式有個特點, 在構建的時候, 能夠將需要的約束也一併繫結到自身裡頭, 所以即使函式f退出了, 變數a釋放了, 但是不要緊, 函式v已經繫結好了相應的約束了, 自然而然也就能輸出4。