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