摘要:真正管理這些名子的事物就是本文的主角命名空間。閉包命名空間閉包函數(shù)的名稱空間引入。函數(shù)調(diào)用時產(chǎn)生新的局部命名空間函數(shù)返回結(jié)果拋出異常時釋放命名空間,每一次遞歸都生成一個命名空間。標(biāo)識符產(chǎn)生地點(diǎn)決定標(biāo)識符所處的命名空間。
懶得掃全文的童鞋,可以直接跳到最后看總結(jié)。
我們先從一個簡單的栗子說起:
a 文件中有變量 va 以及類 A,b 文件導(dǎo)入 a 中class A ,并打印出 A:
#a.py va = ["dobi", "a", "dog"] print("a1", id(va)) class A(): def __init__(self): pass def rtn(self): global va va.insert(1,"is") print("a3", id(va)) return va print("a2", va) #b.py from a import A print("b", A)
執(zhí)行 b 文件的結(jié)果為:
Reloaded modules: a a1 2407907960200 a2 ["dobi", "a", "dog"] b
可以發(fā)現(xiàn),雖然 b 只是導(dǎo)入了 a 中的 class A,但導(dǎo)入這個過程卻執(zhí)行了整個 a 文件,那么我們是否能夠在 b 中訪問 a 中的全局變量 va 呢:
print(va) # NameError: name "va" is not defined print(a.va) # NameError: name "a" is not defined print(b.va) # NameError: name "b" is not defined
嘗試了各類調(diào)用方法,發(fā)現(xiàn)都無法正常訪問 a 的全局變量 va,既然 b 的導(dǎo)入執(zhí)行了整個 a 文件,甚至還打印出了 va 的 id 和值,又為什么無法在 b 中調(diào)用 va 呢?
這個問題所涉及到的內(nèi)容就是:命名空間。
但在開始正題之前,我們需要闡明若干概念:
一些基本概念的澄清 對象Python 一切皆對象,每個對象都具有 一個ID、一個類型、一個值;對象一旦建立,ID 便不會改變,可以直觀的認(rèn)為 ID 就是對象在內(nèi)存中的地址:
a = [1, 2] b = a id(a) # 2407907978632 id(b) # 2407907978632 b[1] = 3 a # [1, 3]
上例 a, b 共享了同一個 ID、同一個值、同一個類型。因此 a, b 表達(dá)的是同一個對象,但 a, b 又明顯是不同的,比如一個叫 "a" 一個叫 "b"...既然是同一個對象,為什么又有不同的名字呢?難道名字不是對象的屬性?
標(biāo)識符事實(shí)確實(shí)如此,這是 Python 比較特殊一點(diǎn):如同"a" "b" 這樣的名稱其實(shí)有一個共同的名字:identifier(注意不要與 ID 混淆了),中文名為“標(biāo)識符”,來解釋一下:
標(biāo)識符:各類對象的名稱,比如函數(shù)名、方法名、類名,變量名、常量名等。
在 Python 中賦值并不會直接復(fù)制數(shù)據(jù),而只是將名稱綁定到對象,對象本身是不知道也不需要關(guān)心(該關(guān)心這個的是程序猿)自己叫什么名字的。一個對象甚至可以指向不同的標(biāo)識符,上例中的"a" "b"便是如此。真正管理這些名子的事物就是本文的主角“命名空間”。
命名空間命名空間(Namespace):名字(標(biāo)識符)到對象的映射。
簡而言之,命名空間可以理解為:記錄對象和對象名字對應(yīng)關(guān)系的空間;現(xiàn)今 Python 的大部分命名空間是通過字典來實(shí)現(xiàn)的,也即一個命名空間就是名字到對象的映射,標(biāo)識符是鍵,對象則是值。
作用域與命名空間相對的一個概念就是“作用域”,那么什么又是作用域呢?
作用域(Scope):本質(zhì)是一塊文本區(qū)域,Python 通過該文本區(qū)域可以直接訪問相應(yīng)的命名空間。
這里需要搞清楚什么是直接訪問:
#x.py a = 1 class A(): def func():pass
python x.py a #直接訪問 # 1 A.func #屬性訪問
Python 中不加 . 的訪問為直接訪問,反之為屬性訪問。
因此可以簡單的將作用域理解為“直接訪問命名空間的一種實(shí)現(xiàn)”,具體而言:
作用域內(nèi)相應(yīng)的命名空間可以被直接訪問;
只有作用域內(nèi)的命名空間才可以被直接訪問(因此并不是所有的命名空間都可以被直接訪問)。
看不懂? 沒關(guān)系,后面會解釋,現(xiàn)在先回到命名空間這個話題上,我們經(jīng)常接觸的命名空間有四類:
LEGB LEGB 命名空間這四類命名空間可以簡記為 LEGB:
局部命名空間(local):指的是一個函數(shù)或者一個類所定義的名稱空間;包括函數(shù)的參數(shù)、局部變量、類的屬性等。
閉包命名空間(enclosing function):閉包函數(shù) 的名稱空間(Python 3 引入)。
全局命名空間(global):讀入一個模塊(也即一個.py文檔)后產(chǎn)生的名稱空間。
內(nèi)建命名空間(builtin):Python 解釋器啟動時自動載入__built__模塊后所形成的名稱空間;諸如 str/list/dict...等內(nèi)置對象的名稱就處于這里。
為了說清楚這幾類命名空間,舉個栗子:
#c.py v1 = "a global var" def func(v): v2 = "a local var" def inn_func(): v3 = v2 + v return v3 return inn_func
內(nèi)建命名空間比較好理解,我們重點(diǎn)講解下其他三個:
"v1" 為全局變量 v1 的名子,其所處的命名空間為全局命名空間;需要注意的是全局命名空間包括 "func" 但不包括 func 的參數(shù)和內(nèi)部變量。
func 囊括 "v"、"v2" 和 "inn_func" 名稱的空間為局部命名空間;
執(zhí)行 func 后,func 的作用域釋放(或許遺忘更合適),并返回了綁定了 v 和 v2 變量的閉包函數(shù) inn_func,此時閉包所具有命名空間即為閉包命名空間,因此局部命名空間和閉包命名空間是相對而言的,對于父函數(shù) func 而言,兩者具有產(chǎn)生時間上的差異。
LEGB 訪問規(guī)則通過上面描述,我們發(fā)現(xiàn) LEGB 四類命名空間本身具有明顯的內(nèi)外層級概念,而這種層級概念正是構(gòu)建作用域的前提:作用域依據(jù)這種層級概念將不同類型的命名空間組織起來并劃歸到不同層級的作用域,然后定義好不同層級作用域之間的訪問規(guī)則,從而實(shí)現(xiàn)命名空間的直接訪問:
LEGB 訪問規(guī)則: 同樣的標(biāo)識符在各層命名空間中可以被重復(fù)使用而不會發(fā)生沖突,但 Python 尋找一個標(biāo)識符的過程總是從當(dāng)前層開始逐層往上找,直到首次找到這個標(biāo)識符為止:
#d.py v1 = 1 v2 = 3 def f(): v1 = 2 print(1, v1) print(2, v2) f() print(3, v1)
1 2 2 3 3 1
上例中,全局變量和函數(shù) f 都定義了 變量 v1,結(jié)果 Python 會優(yōu)先選擇 f 的局部變量 v1 ,對于 f 內(nèi)并未定義的變量 v2 ,Python 會向上搜尋全局命名空間,讀取全局變量 v2 后打印輸出。
global 和 nonlocal 語句 global 和 nonlocal 的作用如前所述,對于上層變量,python 允許直接讀取,但是卻不可以在內(nèi)層作用域直接改寫上層變量,來看一個典型的閉包結(jié)構(gòu):
#e.py gv = ["a", "global", "var"] def func(v): gv = ["gv"] + gv #UnboundLocalError:local variable "gv" referenced before assignment lv = [] def inn_func(): lv = lv + [v] #UnboundLocalError:local variable "lv" referenced before assignment gv.insert(1, lv[0]) return gv return inn_func
實(shí)際調(diào)用 func()函數(shù)后,上面兩處對 gv 和 lv 進(jìn)行賦值操作的地方都會發(fā)生 UnboundLocalError:因?yàn)?Python 在執(zhí)行函數(shù)前,會首先生成各層命名空間和作用域,因此 Python 在執(zhí)行賦值前會將func 內(nèi)的 "gv" "lv" 寫入局部命名空間和閉包命名空間,當(dāng) Python 執(zhí)行賦值時會在局部作用域、閉包作用域內(nèi)發(fā)現(xiàn)局部命名空間和閉包命名空間內(nèi)已經(jīng)具有"gv" 和 "lv" 標(biāo)識符,但這兩個非全局標(biāo)識符在該賦值語句執(zhí)行之前并沒有被賦值,也即沒有對象與標(biāo)識符關(guān)聯(lián),因此無法參與四則運(yùn)算,從而引發(fā)錯誤;但這段程序本意可能只是想讓具有對象的全局變量gv 和局部變量 lv 參與運(yùn)算,為了避免類似的情況發(fā)生,Python 便引入了 global、nonlocal 語句就來說明所修飾的 gv、lv 分別來自全局命名空間和局部命名空間,聲明之后,就可以在 func 和 inn_func 內(nèi)直接改寫上層命名空間內(nèi) gv 和 lv 的值:
#f.py gv = ["a", "global", "var"] def func(v): global gv gv = ["gv"] + gv lv = [] print(id(lv)) def inn_func(): nonlocal lv lv = lv + [v] print(id(lv)) gv.insert(1, lv[0]) return gv return inn_func
a = func("is") # 2608229974344 a() # 2608229974344 # ["gv", "is", "a", "global", "var"] print(gv) # ["gv", "is", "a", "global", "var"]
如上,全局變量 gv 值被函數(shù)改寫了, inn_func 修改的也確實(shí)是父函數(shù) lv的值 (依據(jù) ID 判斷)。
借殼那么是不是不使用 global 和 nonlocal 就不能達(dá)到上面的目的呢?來看看這段程序:
#g.py gv = ["a", "global", "var"] def func(v): gv.insert(0, "gv") lv = [] print(id(lv)) def inn_func(): lv.append(v) print(id(lv)) gv.insert(1, lv[0]) return gv return inn_func
執(zhí)行的結(jié)果:
a = func("is") # 2608110869168 a() # 2608110869168 # ["gv", "is", "a", "global", "var"] print(gv) # ["gv", "is", "a", "global", "var"]
可以發(fā)現(xiàn),執(zhí)行結(jié)果同上面完全一致,問題自然來了:“為什么不用 global nonlocal 也可以改寫全局變量gv和父函數(shù)變量lv的值?
為了看清楚這個過程,我們將上面的gv.insert(0, "gv") lv.append(v) 改寫為 gv[0:0] = ["gv"] lv[:] = [v]:
#h.py gv = ["a", "global", "var"] def func(v): gv[0:0] = ["gv"] lv = [] print(id(lv)) def inn_func(): lv[:] = [v] print(id(lv)) gv.insert(1, lv[0]) return gv return inn_func
執(zhí)行結(jié)果:
a = func("is") # 2608229959496 a() # 2608229959496 # ["gv", "is", "a", "global", "var"]
同 g.py 文件的執(zhí)行結(jié)果完全一致,事實(shí)上兩者之間的內(nèi)在也是完全一樣的。
So 我們其實(shí)改寫的不是 gv 和 lv ,而是 gv 和 lv 的元素 gv[0:0] 和 lv[:] 。因此,不需要 global 和 nonlocal 修飾就可以直接改寫,這就是“借殼”,在 nonlocal 尚未引入 Python 中,比如 Python 2.x 若要在子函數(shù)中改寫父函數(shù)變量的值就得通過這種方法。
當(dāng)然借殼蘊(yùn)藏著一個相對復(fù)雜的標(biāo)識符創(chuàng)建的問題:比如子函數(shù)通過借殼修改父函數(shù)變量lv的值,那么子函數(shù)的標(biāo)識符lv是怎么綁定到父函數(shù)變量lv的值 ID 的上的?
關(guān)于這個問題,這里有個問答就是討論這個的:python的嵌套函數(shù)中局部作用域問題?
global 和 nonlocal 語句對標(biāo)識符創(chuàng)建的不同影響另外,需要注意的是:global 語句只是聲明該標(biāo)識符引用的變量來自于全局變量,但并不能直接在當(dāng)前層創(chuàng)建該標(biāo)識符;nonlocal 語句則會在子函數(shù)命名空間中創(chuàng)建與父函數(shù)變量同名的標(biāo)識符:
#j.py gv = "a global var" def func(): global gv lv = "a local var" print(locals()) def inn_func(): nonlocal lv global gv print(locals()) return inn_func
執(zhí)行結(jié)果:
c = func() {"lv": "a local var"} #運(yùn)行 `func` 函數(shù)后,`global` 語句并未將 `gv` 變量引入局部命名空間 c() {"lv": "a local var"} #運(yùn)行閉包函數(shù)后,`nonlocal` 語句將父函數(shù)變量 `lv` 引入閉包命名空間
之所以 nonlocal 語句與 global 語句的處置不同,在于全局變量的作用域生存期很長,在模塊內(nèi)隨時都可以訪問,而父函數(shù)的局部作用域在父函數(shù)執(zhí)行完畢后便會直接釋放,因此 nonlocal 語句必須將父函數(shù)變量的標(biāo)識符和引用寫入閉包命名空間。
命名空間的生命周期 創(chuàng)建規(guī)則實(shí)際上,到這里其實(shí)還有一個重要的重要問題沒有解決:“標(biāo)識符并不是天生就在命名空間內(nèi)的,命名空間也不是平白無故就產(chǎn)生的,那么命名空間是在什么時候被創(chuàng)建?又是在什么時候被刪除的呢?”
規(guī)則有四:
內(nèi)建命名空間在 Python 解釋器啟動時創(chuàng)建,之后會一直存在;
模塊的全局命名空間在模塊定義被讀入時創(chuàng)建,通常模塊命名空間也會保持到解釋器退出。
函數(shù)調(diào)用時產(chǎn)生新的局部命名空間;函數(shù)返回結(jié)果、拋出異常時釋放命名空間,每一次遞歸都生成一個命名空間。
標(biāo)識符產(chǎn)生地點(diǎn)決定標(biāo)識符所處的命名空間。
這四點(diǎn)就是拿來秒懂的!不過,仍然有一點(diǎn)常常被忽視:類的命名空間:
類的局部命名空間首先,函數(shù)和類執(zhí)行時都會產(chǎn)生局部命名空間,但類的執(zhí)行機(jī)制不同于函數(shù):
#i.py def a(): print("function") class A(): print(1) class B(): print(2) class C(): print(3)
執(zhí)行文件,結(jié)果為:
1 2 3
如上,類就是一個可執(zhí)行的代碼塊,只要該類被加載,就會被執(zhí)行,這一點(diǎn)不同于函數(shù)。
類之所以這么設(shè)計(jì)的原因在于:類是創(chuàng)建其他實(shí)例(生成其他的類或者具體的對象)的對象,因此必須在實(shí)例之前被創(chuàng)建,而類又可能涉及到與其他類的繼承、重載等一系列問題,故在代碼加載時就被創(chuàng)建利于提高效率和降低邏輯復(fù)雜度。
其次,與函數(shù)不同的是,類的局部命名空間并非作用域
class A(): a = 1 b = [a + i for i in range(3)] #NameError: name "a" is not defined
執(zhí)行上段代碼,我們可以發(fā)現(xiàn)在類 A 內(nèi)列表推導(dǎo)式無法調(diào)取 a 的值,但函數(shù)卻可以:
def func(): a = 1 b = [a + i for i in range(3)] print(b) func() #[1, 2, 3]
因此,A 中的 a 不同于函數(shù) func 中的 a 在局部命名空間中可以被任意讀取,之所以說是“不可以被任意”讀取而不是“不可被讀取”,原因在于在類A 的局部空間內(nèi),a 其實(shí)一定程度上是可以直接被讀取的:
class A(): a = 1 c = a + 2
執(zhí)行上段代碼后:
A.c #3
而上例中 b 的賦值操作不能執(zhí)行,原因在于列表推導(dǎo)式會創(chuàng)建自己的局部命名空間,因此難以訪問到 a。
編譯與局部命名空間Python 是動態(tài)語言,很多行為是動態(tài)發(fā)生的,但 Python 自身也在不斷進(jìn)步,比如為了提高效率,有些行為會在編譯時候完成,局部變量的創(chuàng)建就是如此:
def func(): a = 1 def inn_func(): print(a) # error a = 2 # error inn_func()
上段程序還未執(zhí)行,就提示存在有語法錯誤,原因在于python 解釋器發(fā)現(xiàn) inn_func 內(nèi)存在自身的 a 變量,但卻在聲明之前就被 print 了。
總結(jié)啰嗦了這么多,終于該結(jié)尾了!
我們再來回過頭來看下文章開頭的栗子:
1、為什么 b.py 只是導(dǎo)入 a.py 中的 class A,卻執(zhí)行了整個 a.py 文件?
答:因?yàn)?Python 并不知道 class A 在 a.py 文檔的何處,為了能夠找到 class A,Python 需要執(zhí)行整個文檔。
2、為什么 b.py 的導(dǎo)入執(zhí)行了整個 a.py 文檔,卻在 b 中難以調(diào)用 a 的全局變量 va?
答:Python 的全局變量指的是模塊全局,因此不可以跨文檔,因此 global 語句也是不可以跨文檔的。另外, b 只是導(dǎo)入了 a 的 class A,因此并不會導(dǎo)入 a 中所有的標(biāo)識符,所以 類似a.va 這樣的調(diào)用也是不起作用的。
關(guān)于命名空間:
1、賦值、定義類和函數(shù)都會產(chǎn)生新的標(biāo)識符;
2、全局變量的標(biāo)識符不能跨文檔;
3、各級命名空間相互獨(dú)立互不影響;
4、Python 總是從當(dāng)前層逐漸向上尋找標(biāo)識符;
5、內(nèi)層作用域若想直接修改上層變量,需要通過 global nonlocal 語句先聲明;
6、單純的 global 語句并不能為所在層級創(chuàng)建相應(yīng)標(biāo)識符,但 nonlocal 語句可以在閉包空間中創(chuàng)建相應(yīng)標(biāo)識符;
7、類的局部命名空間不是作用域。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/37781.html
摘要:在內(nèi)置命名空間不能使用全局和局部的名字。可以形象地理解成內(nèi)置命名空間具有最高級別,不需要定義就可以使用,全局命名空間次之,最低級是局部命名空間。 python中的命名空間分三種: 內(nèi)置的命名空間,在啟動解釋器的時候自動加載進(jìn)內(nèi)存的各種名字所在的空間,比如print,input等不需要定義就可以使用的名字 全局命名空間,就是從上到下所有我們定義的變量名和函數(shù)名所在的空間,是在程序從上到下...
摘要:正如我們前面舉的張三的例子那樣,不同命名空間中的名稱之間沒有任何關(guān)系。作用域作用域,是一個命名空間可直接發(fā)放完的代碼的文本區(qū)域。刪除也是如此,語句會從局部命名空間的引用中移除對的綁定。 命名空間和作用域的概念我們之前也提到過,比如內(nèi)置函數(shù)globals(),函數(shù)中變量的作用域,模塊使用的import等等。這些可能讓我們對這兩個概念有了大致的理解。本節(jié)再詳細(xì)探討一下。 showImg(h...
摘要:內(nèi)置函數(shù)們能夠被提拔出來,這就意味著它們皆有獨(dú)到之處,有用武之地。因此,掌握內(nèi)置函數(shù)的用法,就成了我們應(yīng)該點(diǎn)亮的技能。報錯包含了內(nèi)置命名空間中的名稱,在控制臺中輸入,就能發(fā)現(xiàn)很多內(nèi)置函數(shù)異常和其它屬性的名稱。 Python 提供了很多內(nèi)置的工具函數(shù)(Built-in Functions),在最新的 Python 3 官方文檔中,它列出了 69 個。 大部分函數(shù)是我們經(jīng)常使用的,例如 p...
摘要:正如儒家經(jīng)典所闡述修身齊家治國平天下。除此之外,模塊還有如下最基本的屬性在一個模塊的全局空間里,有些屬性是全局起作用的,稱之為全局變量,而其它在局部起作用的屬性,會被稱為局部變量。 導(dǎo)讀:Python貓是一只喵星來客,它愛地球的一切,特別愛優(yōu)雅而無所不能的 Python。我是它的人類朋友豌豆花下貓,被授權(quán)潤色與發(fā)表它的文章。如果你是第一次看到這個系列文章,那我強(qiáng)烈建議,請先看看它寫的前...
閱讀 2947·2021-11-23 09:51
閱讀 3200·2021-11-12 10:36
閱讀 3233·2021-09-27 13:37
閱讀 3193·2021-08-17 10:15
閱讀 2615·2019-08-30 15:55
閱讀 2781·2019-08-30 13:07
閱讀 814·2019-08-29 16:32
閱讀 2672·2019-08-26 12:00