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