Python學習之名字,作用域,名字空間(下)
前言:
這里再回顧一下函數(shù)的local空間,首先我們往global空間添加一個鍵值對相當于定義一個全局變量,那么如果往函數(shù)的local空間里面添加一個鍵值對,是不是也等價于創(chuàng)建了一個局部變量呢?
def f1(): locals()["name "] = "夏色祭" try: print(name) except Exception as e: print(e) f1() # name 'name' is not defined
對于全局變量來講,變量的創(chuàng)建是通過向字典添加鍵值對的方式實現(xiàn)的。因為全局變量會一直在變,需要使用字典來動態(tài)維護。
但對于函數(shù)來講,內(nèi)部的變量是通過靜態(tài)方式存儲和訪問的,因為局部作用域中存在哪些變量在編譯的時候就已經(jīng)確定了,我們通過PyCodeObject的co_varnames即可獲取內(nèi)部都有哪些變量。
所以,雖然我們說查找是按照LGB的方式查找,但是訪問函數(shù)內(nèi)部的變量其實是靜態(tài)訪問的,不過完全可以按照LGB的方式理解。
因此名字空間是Python的靈魂,它規(guī)定了Python變量的作用域,使得Python對變量的查找變得非常清晰。
LEGB規(guī)則
而從Python2.2開始,由于引入了嵌套函數(shù),所以最好的方式應該是內(nèi)層函數(shù)找不到某個變量時先去外層函數(shù)找,而不是直接就跑到global空間里面找。
那么此時的規(guī)則就是LEGB:
a = 1 def foo(): a = 2 def bar(): print(a) return bar f = foo() f() """ 2 """
調(diào)用f,實際上調(diào)用的是函數(shù)bar,最終輸出的結(jié)果是2。如果按照LGB的規(guī)則來查找的話,由于函數(shù)bar的作用域沒有a、那么應該到全局里面找,打印的結(jié)果是1才對。
但是我們之前說了,作用域僅僅是由文本決定的,函數(shù)bar位于函數(shù)foo之內(nèi),所以函數(shù)bar定義的作用域內(nèi)嵌于函數(shù)foo的作用域之內(nèi)。換句話說,函數(shù)foo的作用域是函數(shù)bar的作用域的直接外圍作用域。
所以應該先從foo的作用域里面找,如果沒有那么再去全局里面找。而作用域和名字空間是對應的,所以最終打印了2。
另外在執(zhí)行f = foo()的時候,會執(zhí)行函數(shù)foo中的def bar():語句,這個時候解釋器會將a=2與函數(shù)bar捆綁在一起,然后返回,這個捆綁起來的整體就叫做閉包。
所以:閉包 = 內(nèi)層函數(shù) + 引用的外層作用域
這里顯示的規(guī)則就是LEGB,其中E表示enclosing,代表直接外圍作用域。
global表達式
有一個很奇怪的問題,最開始學習Python的時候,筆者也為此困惑了一段時間,下面來看一下。
a = 1 def foo(): print(a) foo() """ 1 """
首先這段代碼打印1,這顯然是沒有問題的,不過下面問題來了。
a = 1 def foo(): print(a) a = 2 foo() """ UnboundLocalError: local variable 'a' referenced before assignment """
僅僅是在print語句后面新建了一個變量a,結(jié)果就報錯了,提示局部變量a在賦值之前就被引用了,這是怎么一回事,相信肯定有人為此困惑。
而想弄明白這個錯誤的原因,需要深刻理解兩點:
- 一個賦值語句所定義的變量,在這個賦值語句所在的整個作用域內(nèi)都是可見的;
- 函數(shù)中的變量是靜態(tài)存儲、靜態(tài)訪問的, 內(nèi)部有哪些變量在編譯的時候就已經(jīng)確定;
在編譯的時候,因為a = 2這條語句,所以知道函數(shù)中存在一個局部變量a,那么查找的時候就會在當前作用域中查找。但是還沒來得及賦值,就print(a)了,所以報錯:局部變量a在賦值之前就被引用了。但如果沒有a = 2這條語句則不會報錯,因為知道局部作用域中不存在a這個變量,所以會找全局變量a,從而打印1
更有趣的東西隱藏在字節(jié)碼當中,我們可以通過反匯編來查看一下:
import dis a = 1 def g(): print(a) dis.dis(g) """ 7 0 LOAD_GLOBAL 0 (print) 2 LOAD_GLOBAL 1 (a) 4 CALL_FUNCTION 1 6 POP_TOP 8 LOAD_CONST 0 (None) 10 RETURN_VALUE """ def f(): print(a) a = 2 dis.dis(f) """ 12 0 LOAD_GLOBAL 0 (print) 2 LOAD_FAST 0 (a) 4 CALL_FUNCTION 1 6 POP_TOP 13 8 LOAD_CONST 1 (2) 10 STORE_FAST 0 (a) 12 LOAD_CONST 0 (None) 14 RETURN_VALUE """
中間的序號代表字節(jié)碼的偏移量,我們先看第二條,g的字節(jié)碼是LOAD_GLOBAL,意思是在global名字空間中查找;而f的字節(jié)碼是LOAD_FAST,表示在local名字空間中查找。因此結(jié)果說明Python采用了靜態(tài)作用域策略,在編譯的時候就已經(jīng)知道了名字藏身于何處。
而且上面的例子也表明,一旦函數(shù)內(nèi)有了對某個名字的賦值操作,這個名字就會在作用域內(nèi)可見,就會出現(xiàn)在local名字空間中。換句話說,會遮蔽外層作用域中相同的名字。
當然Python也為我們精心準備了global關鍵字,讓我們在函數(shù)內(nèi)部修改全局變量。比如函數(shù)內(nèi)部出現(xiàn)了global a,就表示我后面的a是全局的,直接到global名字空間里面去找,不要在local空間里面找了。
a = 1 def bar(): def foo(): global a a = 2 return foo bar()() print(a) # 2 # 當然,也可以通過globals函數(shù)拿到名字空間 # 然后直接修改里面的鍵值對
但如果外層函數(shù)里面也出現(xiàn)了變量a,而我們想修改的也是外層函數(shù)的a、不是全局的a,這時該怎么辦呢?Python同樣為我們準備了關鍵字: nonlocal,但是使用nonlocal的時候,必須是在內(nèi)層函數(shù)里面。
a = 1 def bar(): a = 2 def foo(): nonlocal a a = "xxx" return foo bar()() print(a) # 1 # 外界依舊是1,但是bar里面的a已經(jīng)被修改了
屬性引用與名字引用
屬性引用實質(zhì)上也是一種名字引用,其本質(zhì)都是到名字空間中去查找一個名字所引用的對象。這個就比較簡單了,比如a.xxx,就是到a里面去找屬性xxx,這個規(guī)則是不受LEGB作用域限制的,就是到a里面查找,有就是有、沒有就是沒有。
但是有一點需要注意,我們說查找會按照LEGB規(guī)則,但這必須限制在自身所在的模塊內(nèi),如果是多個模塊就不行了。舉個栗子:
# a.py print(name) # b.py name = "夏色祭" import a
關于模塊的導入我們后續(xù)會詳細說,總之目前在b.py里面執(zhí)行的import a,你可以簡單認為就是把a.py里面的內(nèi)容拿過來執(zhí)行一遍即可,所以這里相當于print(name)。
但是執(zhí)行b.py的時候會提示變量name沒有被定義,可把a導進來的話,就相當于print(name),而我們上面也定義name這個變量了呀。
顯然,即使我們把a導入了進來,但是a.py里面的內(nèi)容依舊是處于一個模塊里面。而我們也說了,名稱引用雖然是LEGB規(guī)則,但是無論如何都無法越過自身所在的模塊。print(name)在a.py里面,而變量name被定義在b.py里面,所以不可能跨過模塊a的作用域去訪問模塊b里面的name,因此在執(zhí)行 import a 的時候會拋出 NameError。
所以我們發(fā)現(xiàn),雖然每個模塊內(nèi)部的作用域規(guī)則有點復雜,因為要遵循LEGB;但模塊與模塊之間的作用域還是劃分的很清晰的,就是相互獨立。
關于模塊,我們后續(xù)會詳細說??傊ㄟ^ . 的方式,本質(zhì)上都是去指定的名字空間中查找對應的屬性。
屬性空間
我們知道,自定義的類里面如果沒有__slots__,那么這個類的實例對象都會有一個屬性字典。
class Girl: def __init__(self): self.name = "古明地覺" self.age = 16 g = Girl() print(g.__dict__) # {'name': '古明地覺', 'age': 16} # 對于查找屬性而言, 也是去屬性字典中查找 print(g.name, g.__dict__["name"]) # 古明地覺 古明地覺 # 同理設置屬性, 也是更改對應的屬性字典 g.__dict__["gender"] = "female" print(g.gender) # female
當然模塊也有屬性字典,本質(zhì)上和類的實例對象是一致的。
import builtins print(builtins.str) # <class 'str'> print(builtins.__dict__["str"]) # <class 'str'> # 另外,有一個內(nèi)置的變量 __builtins__,和導入的 builtins 等價 print(__builtins__ is builtins) # True
另外這個__builtins__位于 global名字空間里面,然后獲取global名字空間的globals又是一個內(nèi)置函數(shù),于是一個神奇的事情就出現(xiàn)了。
print(globals()["__builtins__"].globals()["__builtins__"]. globals()["__builtins__"].globals()["__builtins__"]. globals()["__builtins__"].globals()["__builtins__"] ) # <module 'builtins' (built-in)> print(globals()["__builtins__"].globals()["__builtins__"]. globals()["__builtins__"].globals()["__builtins__"]. globals()["__builtins__"].globals()["__builtins__"].list("abc") ) # ['a', 'b', 'c']
所以global名字空間和builtin名字空間,都保存了指向彼此的指針,不管套娃多少次,都是可以的。
小結(jié)
在 Python 中,一個名字(變量)的可見范圍由作用域決定,而作用域由語法靜態(tài)劃分,劃分規(guī)則提煉如下:
- .py文件(模塊)最外層為全局作用域;
- 遇到函數(shù)定義,函數(shù)體形成子作用域;
- 遇到類定義,類定義體形成子作用域;
- 名字僅在其作用域以內(nèi)可見;
- 全局作用域?qū)ζ渌凶饔糜蚩梢姡?/li>
- 函數(shù)作用域?qū)ζ渲苯幼幼饔糜蚩梢?,并且可以傳遞(閉包);
與作用域相對應, Python在運行時借助PyDictObject對象保存作用域中的名字,構(gòu)成動態(tài)的名字空間 。
這樣的名字空間總共有 4 個:
- 局部名字空間(local):不同的函數(shù),局部名字空間不同,可以通過調(diào)用 locals 獲?。?/li>
- 全局名字空間(global):全局唯一,可以通過調(diào)用 globals 獲取;
- 閉包名字空間(enclosing);
- 內(nèi)置名字空間(builtin):可以通過調(diào)用 builtins__.__dict 獲取;
查找名字時會按照LEGB規(guī)則查找,但是注意:無法跨越文件本身,也就是按照自身文件的LEGB。如果屬性查找都找到builtin空間了,那么證明這已經(jīng)是最后的倔強。如果builtin空間再找不到,那么就只能報錯了,不可能跑到其它文件中找。
到此這篇關于Python學習之名字,作用域,名字空間(下)的文章就介紹到這了,更多相關Python名字空間內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Python機器學習NLP自然語言處理基本操作之命名實例提取
自然語言處理(?Natural?Language?Processing,?NLP)是計算機科學領域與人工智能領域中的一個重要方向。它研究能實現(xiàn)人與計算機之間用自然語言進行有效通信的各種理論和方法2021-11-11Python圖像處理庫PIL的ImageEnhance模塊使用介紹
這篇文章主要介紹了Python圖像處理庫PIL的ImageEnhance模塊使用介紹,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-02-02Python Charles抓包配置實現(xiàn)流程圖解
這篇文章主要介紹了Python Charles抓包實現(xiàn)流程圖解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-09-09