小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

《源碼探秘 CPython》62. 函數(shù)的 local 名字空間

 古明地覺O_o 2022-12-08 發(fā)布于北京

在看完函數(shù)的參數(shù)解析之后,我們來聊一聊函數(shù)的 local 名字空間。

我們知道函數(shù)的參數(shù)和函數(shù)內(nèi)部定義的變量都屬于局部變量,均是通過靜態(tài)的方式訪問的。

x = 123
def foo1(): global x a = 1 b = 2
# a 和 b 是局部變量,x 是全局變量,因此是 2print(foo1.__code__.co_nlocals) # 2

def foo2(a, b): pass
print(foo2.__code__.co_nlocals) # 2

def foo3(a, b): a = 1 b = 2 c = 3
print(foo3.__code__.co_nlocals) # 3

無論是參數(shù)還是內(nèi)部新創(chuàng)建的變量,本質(zhì)上都是局部變量。并且我們發(fā)現(xiàn)如果函數(shù)內(nèi)部定義的變量和參數(shù)名稱一致,那么參數(shù)就沒用了。

這很好理解,因為本質(zhì)上就相當(dāng)于重新賦值了,此時外面無論給函數(shù)foo2的參數(shù)a、b傳什么值,最終都會變成 1 和 2。所以其實局部變量的實現(xiàn)機(jī)制和函數(shù)參數(shù)的實現(xiàn)機(jī)制是一致的。

按照之前的理解,當(dāng)訪問一個全局變量時,會去訪問 global 名字空間,而這也確實如此。但是當(dāng)訪問函數(shù)的局部變量時,是不是訪問其內(nèi)部的 local 名字空間呢? 

之前我們說過 Python 變量的訪問是有規(guī)則的,按照本地、閉包、全局、內(nèi)置的順序去查找,所以當(dāng)然會首當(dāng)其沖去 local 名字空間里面查找啊。

但不幸的是,在調(diào)用函數(shù)期間,虛擬機(jī)通過_PyFrame_New_NoTrack創(chuàng)建棧幀對象時,這個至關(guān)重要的 local 名字空間并沒有被創(chuàng)建。

//frameobject.cPyFrameObject* _Py_HOT_FUNCTION_PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,                     PyObject *globals, PyObject *locals){    //...        f->f_locals = NULL;        f->f_trace = NULL;    //...    }

對于模塊而言,它的 f_locals和 f_globals指向是同一個PyDictObject;但對于函數(shù)而言,f_locals 卻是 NULL。那么問題來了,這些重要的符號到底存儲在什么地方呢?

顯然我們知道是存儲在co_varnames中,但你們就裝作不知道配合我一下好吧(#^.^#)

我們先來舉個栗子:

def foo(a, b):    c = a + b    print(c)

它的字節(jié)碼如下:


棧幀的 f_localsplus 這段連續(xù)內(nèi)存(數(shù)組)是給四個老鐵使用的,分別是:局部變量、cell對象、free對象、運行時棧。而我們看到字節(jié)碼偏移量為 6 和 10 的兩條指令分別是:STORE_FAST 和 LOAD_FAST,所以它和我們之前分析參數(shù)的時候是一樣的,都是存儲在 f_localsplus 的第一段內(nèi)存中。

此時我們對局部變量 c 的藏身之處已經(jīng)了然于心,但是為什么函數(shù)的實現(xiàn)沒有使用 local 名字空間呢?答案很簡單,因為函數(shù)內(nèi)部的局部變量有多少,在編譯的時候就已經(jīng)確定了,個數(shù)是不會變的。因此編譯時就能確定局部變量占用的內(nèi)存大小,也能確定訪問局部變量的字節(jié)碼指令應(yīng)該如何訪問內(nèi)存。

def foo(a, b):    c = a + b    print(c)
print(foo.__code__.co_varnames) # ('a', 'b', 'c')

我們看到符號 c 位于符號表中索引為 2 的位置(編譯時就已確定),那么通過 f_localsplus[2] 即可拿到變量 c 對應(yīng)的值。

這個過程是基于數(shù)組索引實現(xiàn)的靜態(tài)查找,它的效率非常高。而 local 空間是一個字典,雖然字典也是經(jīng)過高度優(yōu)化的,但肯定沒有靜態(tài)查找快。

因此,盡管虛擬機(jī)為函數(shù)實現(xiàn)了 local 空間(初始為 NULL,后續(xù)訪問的時候會進(jìn)行填充),但是在變量查找時卻沒有使用它,原因就是為了更高的效率,而且函數(shù)是一等公民,使用頻率很高。

結(jié)論:雖然查找的時候是按照 LEGB 規(guī)則,但其實局部變量是靜態(tài)訪問的,不過完全可以按照 LEGB 的方式來理解。

我們從 Python 的層面來演示一下:

x = 1
def foo(): globals()["x"] = 2    foo()print(x) # 2

我們在函數(shù)內(nèi)部訪問了 global 名字空間,而 global 空間全局唯一,在 Python 層面上就是一個字典。

  • 查找變量 x,等價于 globals()["x"];

  • 給變量 x 賦值為 123,等價于 globals()["x"] = 123;

因此在執(zhí)行完 foo() 之后,全局變量 x 就被修改了。但 local 名字空間也是如此嗎?我們來看看:

def foo():    x = 1    locals()["x"] = 2    print(x)

foo() # 1

我們按照相同的套路,卻并沒有成功,這是為什么?原因就是我們剛才解釋的那樣,函數(shù)內(nèi)部有哪些局部變量在編譯時就已經(jīng)確定好了,存儲在符號表 co_varnames 中,查詢的時候是從 f_localsplus 中靜態(tài)查找的,而不是從 locals() 中查找。

locals() 不像 globals(),雖然它們都是字典,但 globals() 全局唯一。我們調(diào)用 globals() 就直接訪問到了存放全局變量的字典,一旦做了更改,肯定會影響外面的全局變量。

而 locals() 則不會,因為局部變量壓根就不是從它這里訪問的,盡管它和 globals() 類似,在函數(shù)中也唯一,也會隨著當(dāng)前的上下文動態(tài)改變。

def foo(a, b):    x = 1    print(locals())    print(id(locals()))    y = 2    print(locals())    print(id(locals()))
foo(1, 2)"""{'a': 1, 'b': 2, 'x': 1}2459571657088{'a': 1, 'b': 2, 'x': 1, 'y': 2}2459571657088"""

我們看到真的就類似于全局名字空間一樣,前后地址沒有變化,但是鍵值對的個數(shù)在增加。因為 locals() 底層會執(zhí)行 PyEval_GetLocals,實際上拿到就是當(dāng)前棧幀對象的 f_locals 屬性。


所以 local 名字空間的表現(xiàn)和 global 名字空間是類似的,都會隨著上下文動態(tài)改變。只是我們知道,局部變量不是從 local 名字空間里面訪問的,不管怎么操作 locals(),都不會影響局部變量。

因此我們可以看到一個比較奇特的現(xiàn)象:

def foo(a, b):    # 當(dāng)前 local 空間只有 a 和 b    d = locals()    print(d)    # 此時多了一個 d    print(locals())    print(d["d"is d["d"]["d"is d["d"]["d"]["d"])
foo(1, 2)"""{'a': 1, 'b': 2}{'a': 1, 'b': 2, 'd': {...}}True"""

仔細(xì)思考一下肯定很好理解,它就有點類似 globals() 與 __builtins__ 之間的關(guān)系:

# __builtins__ 等價于 import builtins as __builtins__x = 123print(    globals()["__builtins__"].globals()["__builtins__"].globals()["x"])  # 123

再看一個例子:

def foo():    locals()["x"] = 1    print(x)
foo()

此時會得到什么結(jié)果估計不用我說了,因為本地、全局、builtin 里面都沒有變量 x,所以報錯。盡管在locals()里面我們設(shè)置了,但局部變量的值不是從它這里獲取的,而是從 f_localsplus 里面。而且查看符號表的話,會發(fā)現(xiàn)里面也沒有 'x' 這個符號。

如果我們設(shè)置一個全局變量呢?

x = 123
def foo(): locals()["x"] = 1 print(x) foo() # 123

顯然此時會訪問全局變量。


我們再來搭配 exec 關(guān)鍵字,區(qū)別會更加明顯。

def foo():    print(locals())  # {}    exec("x = 1")    print(locals())  # {'x': 1}    try:        print(x)    except NameError as e:        print(e)  # name 'x' is not definedfoo()

盡管 locals() 變了,但是依舊訪問不到 x,因為虛擬機(jī)并不知道 exec("x = 1") 是創(chuàng)建一個局部變量,它只知道這是一個函數(shù)調(diào)用。

而 exec("x = 1") 默認(rèn)影響的是當(dāng)前所在的作用域,所以效果就是改變了局部名字空間,里面多了一個 "x": 1 鍵值對。但關(guān)鍵的是,局部變量 x 不是從局部名字空間中查找的,exec 終究還是錯付了人。由于函數(shù) foo 對應(yīng)的 PyCodeObject 對象的符號表中并沒有 x 這個符號,所以報錯了。

exec("x = 1")print(x)  # 1

這么做是可以的,因為 exec 默認(rèn)影響的是當(dāng)前作用域,而這里的當(dāng)前作用域就是全局作用域,所以 global 名字空間會多一個 key 為 "x" 的鍵值對。而全局變量是從global 名字空間中查找的,所以這里沒有問題。

def foo():    # 此時 exec 影響的是全局名字空間    exec("x = 123", globals())    # 這里不會報錯, 但是此時的 x 不是局部變量, 而是全局變量    print(x)
foo()print(x)"""123123"""

再來看一個奇怪的問題:

def foo():    exec("x = 1")    print(locals()["x"])
foo()"""1"""
def bar(): exec("x = 1") print(locals()["x"]) x = 123
bar()"""Traceback (most recent call last): File ..... bar() File ..... print(locals()["x"])KeyError: 'x'"""

這是什么情況?函數(shù) bar只是多了一行賦值語句,為啥就報錯了呢?要想搞懂這個問題,首先要明確兩點:

  • 1. 函數(shù)的局部變量在編譯的時候已經(jīng)確定,并存儲在對應(yīng)的 PyCodeObject 對象的符號表 (co_varnames) 中,這是由語法規(guī)則所決定的;

  • 2. 函數(shù)內(nèi)的局部變量在其整個作用域范圍內(nèi)都是可見的;

為了更好地解釋上面這個例子,這里再舉一個常見的錯誤:

x = 1
def foo(): print(x)
def bar(): print(x) x = 2
print(foo.__code__.co_varnames) # ()print(bar.__code__.co_varnames) # ('x',)

調(diào)用函數(shù)foo沒有問題,但調(diào)用 bar 的時候會報出如下錯誤:UnboundLocalError: local variable 'x' referenced before assignment。

原因就在于上面說的兩個點,函數(shù)內(nèi)的局部變量在編譯的時候已經(jīng)確定,當(dāng)進(jìn)行語法解析的時候,看到了 x=2 這樣的字眼,就知道內(nèi)部會存在一個名為 x 的局部變量。所以對于 bar 函數(shù)而言,符號表中是存在 "x" 這個符號的。

而函數(shù)內(nèi)的局部變量在整個作用域內(nèi)又都是可見的,因此對于函數(shù)bar而言,在 print(x) 的時候知道符號表中存在 "x" 這個符號。那么它也就認(rèn)為局部作用域中存在 x 這個局部變量,因此就不會去找全局變量了,而是去找局部變量。

但是顯然 print(x) 是在 x=2 之前發(fā)生的,所以此時 print(x) 的時候就報錯了。

UnboundLocalError: 局部變量 'x' 在賦值(x=2)之前被引用(print)了

因為 print(x) 的時候,f_localsplus中還沒有對應(yīng)的值與之綁定,或者說 x 此時還是一個 NULL(空指針),并沒有指向一塊合法的內(nèi)存(已存在的 PyObject)。

當(dāng)虛擬機(jī)執(zhí)行到 x=2 之后,x 才會和 2 這個 PyLongObject 對象進(jìn)行綁定,只可惜我們在綁定之前就使用 x 這個變量了,顯然這是不合法的??梢钥匆幌伦止?jié)碼:

我們看到指令是LOAD_FAST,說明加載的是一個局部變量,但這個局部變量的賦值是發(fā)生在LOAD_FAST之后。

那么一開始的那個問題就很好解釋了:

def foo():    exec("x = 1")    print(locals())
def bar(): exec("x = 1") print(locals()) x = 123

foo() # {'x': 1}bar() # {}

對于 foo 而言,結(jié)果符合我們的預(yù)期;但對于 bar 而言,只是多了一個賦值語句,結(jié)果局部空間就變成空字典了。


原因和 
UnboundLocalError 類似,因為 'x' 已經(jīng)在符號表當(dāng)中了,所以 exec("x = 1") 不會再往局部空間中加入這個鍵值對。但如果將 bar 里面的 x=123 改成 y=123,那么顯然輸出的結(jié)果就是一樣的了。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多