Python進(jìn)階_關(guān)于命名空間與作用域(詳解)
寫(xiě)在前面
如非特別說(shuō)明,下文均基于Python3
命名空間與作用于跟名字的綁定相關(guān)性很大,可以結(jié)合另一篇介紹Python名字、對(duì)象及其綁定的文章。
1. 命名空間
1.1 什么是命名空間
Namespace命名空間,也稱名字空間,是從名字到對(duì)象的映射。Python中,大部分的命名空間都是由字典來(lái)實(shí)現(xiàn)的,但是本文的不會(huì)涉及命名空間的實(shí)現(xiàn)。命名空間的一大作用是避免名字沖突:
def fun1(): i = 1 def fun2(): i = 2
同一個(gè)模塊中的兩個(gè)函數(shù)中,兩個(gè)同名名字i之間絕沒(méi)有任何關(guān)系,因?yàn)樗鼈兎謱儆诓煌髅骺臻g。
1.2 命名空間的種類
常見(jiàn)的命名空間有:
built-in名字集合,包括像abs()這樣的函數(shù),以及內(nèi)置的異常名字等。通常,使用內(nèi)置這個(gè)詞表示這個(gè)命名空間-內(nèi)置命名空間
模塊全局名字集合,直接定義在模塊中的名字,如類,函數(shù),導(dǎo)入的其他模塊等。通常,使用全局命名空間表示。
函數(shù)調(diào)用過(guò)程中的名字集合,函數(shù)中的參數(shù),函數(shù)體定義的名字等,在函數(shù)調(diào)用時(shí)被“激活”,構(gòu)成了一個(gè)命名空間。通常,使用局部命名空間表示。
一個(gè)對(duì)象的屬性集合,也構(gòu)成了一個(gè)命名空間。但通常使用objname.attrname的間接方式訪問(wèn)屬性,而不是直接訪問(wèn),故不將其列入命名空間討論。
類定義的命名空間,通常解釋器進(jìn)入類定義時(shí),即執(zhí)行到class ClassName:語(yǔ)句,會(huì)新建一個(gè)命名空間。(見(jiàn)官方對(duì)類定義的說(shuō)明)
1.3 命名空間的生命周期
不同類型的命名空間有不同的生命周期:
內(nèi)置命名空間,在Python解釋器啟動(dòng)時(shí)創(chuàng)建,解釋器退出時(shí)銷(xiāo)毀;
全局命名空間,模塊的全局命名空間在模塊定義被解釋器讀入時(shí)創(chuàng)建,解釋器退出時(shí)銷(xiāo)毀;
局部命名空間,這里要區(qū)分函數(shù)以及類定義。函數(shù)的局部命名空間,在函數(shù)調(diào)用時(shí)創(chuàng)建,函數(shù)返回或者由未捕獲的異常時(shí)銷(xiāo)毀;類定義的命名空間,在解釋器讀到類定義創(chuàng)建,類定義結(jié)束后銷(xiāo)毀。(關(guān)于類定義的命名空間,在類定義結(jié)束后銷(xiāo)毀,但其實(shí)類對(duì)象就是這個(gè)命名空間內(nèi)容的包裝,見(jiàn)官方對(duì)類定義的說(shuō)明)
2. 作用域
2.1 什么是作用域
作用域是Python的一塊文本區(qū)域,這個(gè)區(qū)域中,命名空間可以被“直接訪問(wèn)”。這里的直接訪問(wèn)指的是試圖在命名空間中找到名字的絕對(duì)引用(非限定引用)。這里有必要解釋下直接引用和間接引用:
直接引用;直接使用名字訪問(wèn)的方式,如name,這種方式嘗試在名字空間中搜索名字name。
間接引用;使用形如objname.attrname的方式,即屬性引用,這種方式不會(huì)在命名空間中搜索名字attrname,而是搜索名字objname,再訪問(wèn)其屬性。
2.2 與命名空間的關(guān)系
現(xiàn)在,命名空間持有了名字。作用域是Python的一塊文本區(qū)域,即一塊代碼區(qū)域,需要代碼區(qū)域引用名字(訪問(wèn)變量),那么必然作用域與命名空間之間就有了聯(lián)系。
顧名思義,名字作用域就是名字可以影響到的代碼文本區(qū)域,命名空間的作用域就是這個(gè)命名空間可以影響到的代碼文本區(qū)域。那么也存在這樣一個(gè)代碼文本區(qū)域,多個(gè)命名空間可以影響到它。
作用域只是文本區(qū)域,其定義是靜態(tài)的;而名字空間卻是動(dòng)態(tài)的,只有隨著解釋器的執(zhí)行,命名空間才會(huì)產(chǎn)生。那么,在靜態(tài)的作用域中訪問(wèn)動(dòng)態(tài)命名空間中的名字,造成了作用域使用的動(dòng)態(tài)性。
那么,可以這樣認(rèn)為:
靜態(tài)的作用域,是一個(gè)或多個(gè)命名空間按照一定規(guī)則疊加影響代碼區(qū)域;運(yùn)行時(shí)動(dòng)態(tài)的作用域,是按照特定層次組合起來(lái)的命名空間。
在一定程度上,可以認(rèn)為動(dòng)態(tài)的作用域就是命名空間。在后面的表述中,我會(huì)把動(dòng)態(tài)的作用域與其對(duì)應(yīng)命名空間等同起來(lái)。
2.3 名字搜索規(guī)則
在程序中引用了一個(gè)名字,Python是怎樣搜索到這個(gè)名字呢?
在程序運(yùn)行時(shí),至少存在三個(gè)命名空間可以被直接訪問(wèn)的作用域:
Local
首先搜索,包含局部名字的最內(nèi)層(innermost)作用域,如函數(shù)/方法/類的內(nèi)部局部作用域;
Enclosing
根據(jù)嵌套層次從內(nèi)到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封閉函數(shù)的作用域。如兩個(gè)嵌套的函數(shù),內(nèi)層函數(shù)的作用域是局部作用域,外層函數(shù)作用域就是內(nèi)層函數(shù)的 Enclosing作用域;
Global
倒數(shù)第二次被搜索,包含當(dāng)前模塊全局名字的作用域;
Built-in
最后被搜索,包含內(nèi)建名字的最外層作用域。
程序運(yùn)行時(shí),LGB三個(gè)作用域是一定存在的,E作用域不一定存在;若程序是這樣的:
i = 1 print(i)
局部作用域在哪里呢?我們認(rèn)為(Python Scopes And Namespaces):
Usually, the local scope references the local names of the (textually) current function. Outside functions, the local scope references the same namespace as the global scope: the module's namespace. Class definitions place yet another namespace in the local scope.
一般地,局部作用域引用函數(shù)中定義的名字。函數(shù)之外,局部作用域和全局作用域引用同一個(gè)命名空間:模塊的明星空間。然而類型的局部作用域引用了類定義新的命名空間。
Python按照以上L-E-G-B的順序依次在四個(gè)作用域搜索名字。沒(méi)有搜索到時(shí),Python拋出NameError異常。
2.4 何時(shí)引入作用域我們知道:
我們知道:
在Python中一個(gè)名字只有在定義之后,才能引用。
print(i)
直接引用未定義的名字i,按照搜索規(guī)則,在LGB三個(gè)作用域均沒(méi)有搜索到名字i(LB相同命名空間)。拋出NameError異常:
Traceback (most recent call last): File "scope_test.py", line 15, in <module> print(i) NameError: name 'i' is not defined
那對(duì)于這段代碼呢?
def try_to_define_name(): '''函數(shù)中定義了名字i,并綁定了一個(gè)整數(shù)對(duì)象1''' i = 1 try_to_define_name() print(i) #引用名字i之前,調(diào)用了函數(shù)
在引用名字i之前,明明調(diào)用了函數(shù),定義了名字i,可是還是找不到這個(gè)名字:
Traceback (most recent call last): File "scope_test.py", line 20, in <module> print(i) #引用名字i之前,調(diào)用了函數(shù) NameError: name 'i' is not defined
雖然定義了名字i,但是定義在了函數(shù)的局部作用域?qū)?yīng)的局部命名空間中,按照LEGB搜索規(guī)則,在全局作用域中自然訪問(wèn)不到局部作用域;再者,函數(shù)調(diào)用結(jié)束后,這個(gè)命名空間被銷(xiāo)毀了。
引用名字總是與作用域相關(guān)的,因此:
在Python中一個(gè)名字只有在定義之后,才能在合適的作用域引用。
那么,在定義名字時(shí),就要注意名字定義的作用域了,以免定義后需要訪問(wèn)時(shí)卻找不到。所以,了解Python在何時(shí)會(huì)引入新的作用域很有必要。一般來(lái)說(shuō),B,G兩個(gè)作用域的引入在不能夠通過(guò)代碼操作的,能夠通過(guò)語(yǔ)句引入的作用域只有E,L了。Python中引入新作用域的語(yǔ)句很有限,總的來(lái)說(shuō)只有兩類一個(gè):
函數(shù)定義引入local作用域或者Enclosing作用域;本質(zhì)上,lambda和生成器表達(dá)式也是函數(shù),會(huì)引入新作用域。
類定義引入local作用域;
列表推導(dǎo)式引入local作用域,傳說(shuō)在python2中列表推導(dǎo)式不引入新的作用域
幾個(gè)會(huì)讓有其他高級(jí)語(yǔ)言經(jīng)驗(yàn)的猿困惑的地方:
if語(yǔ)句:
if True: i = 1 print(i) # output: 1,而不是NameError
if語(yǔ)句并不會(huì)引入新的作用域,所以名字綁定語(yǔ)句i = 1與print(i)是在同一個(gè)作用域中。
for語(yǔ)句:
for i in range(6): pass print(i) #output: 5,而不是NameError
for語(yǔ)句同樣不會(huì)引入新的作用域,所以名字i的綁定和重綁定與print(i)在同一個(gè)作用域。這一點(diǎn)Python就比較坑了,因此寫(xiě)代碼時(shí)切忌for循環(huán)名字要與其他名字不重名才行。
import語(yǔ)句:
def import_sys(): '''import sys module''' import sys import_sys() print(sys.path) # NameError: name 'sys' is not defined
這個(gè)算非正常程序員的寫(xiě)法了,import語(yǔ)句在函數(shù)import_sys中將名字sys和對(duì)應(yīng)模塊綁定,那sys這個(gè)名字還是定義在局部作用域,跟上面的例子沒(méi)有任務(wù)區(qū)別。要時(shí)刻切記Python的名字,對(duì)象,這個(gè)其他編程語(yǔ)言不一樣,但是:
打破第一編程語(yǔ)言認(rèn)知的第二門(mén)編程語(yǔ)言,才是值得去學(xué)的好語(yǔ)言。
3. 作用域應(yīng)用
3.1 自由變量可讀不可寫(xiě)
我不太想用“變量”這個(gè)詞形容名字,奈何變量是家喻戶曉了,Python中的自由變量:
If a variable is used in a code block but not defined there, it is a free variable.
如果引用發(fā)生的代碼塊不是其定義的地方,它就是一個(gè)自由變量。專業(yè)一點(diǎn),就是:
引用名字的作用域中沒(méi)有這個(gè)名字,那這個(gè)名字就是自由名字
Note: “自由名字”只是作者YY的,并沒(méi)得到廣泛認(rèn)可。
我們已經(jīng)了解了作用域有LEGB的層次,并按順序搜索名字。按照搜索順序,當(dāng)?shù)蛯幼饔糜虿淮嬖诖阉髅謺r(shí),引用高層作用域存在的名字,也就是自由名字:
[示例1]
def low_scope(): print(s) s = 'upper scope' low_scope()
很清楚,這段代碼的輸出是upper scope。
[示例2]
def low_scope(): s = 'lower scope' s = 'upper scope' low_scope() print(s)
很遺憾,最后的打印語(yǔ)句沒(méi)有按照期待打印出lower scope而是打印了upper scope。
A special quirk of Python is that – if no global statement is in effect – assignments to names always go into the innermost scope.
Python的一個(gè)怪癖是,如果沒(méi)有使用global語(yǔ)句,對(duì)名字的賦值語(yǔ)句通常會(huì)影響最內(nèi)層作用域。
即賦值語(yǔ)句影響局部作用域,賦值語(yǔ)句帶來(lái)的影響是綁定或重綁定,但是在當(dāng)前局部作用域的命名空間中,并沒(méi)有s這個(gè)名字,因此賦值語(yǔ)句在局部作用于定義了同名名字s,這與外層作用域中的s并不沖突,因?yàn)樗鼈兎謱俨煌臻g。
這樣,全局作用域的s沒(méi)有被重綁定,結(jié)果就很好解釋了。
當(dāng)涉及可變對(duì)象時(shí),情況又有所不同了:
[示例3]
def low_scope(): l[0] = 2 l = [1, 2] low_scope() print(l) # [2, 2]
很遺憾,最后的打印語(yǔ)句沒(méi)有按照期待輸出[1, 2]而是輸出了[2, 2]。
上一個(gè)例子的經(jīng)驗(yàn)并不能運(yùn)用在此,因?yàn)閘ist作為一個(gè)可變對(duì)象,l[0] = 2并不是對(duì)名字l的重綁定,而是對(duì)l的第一個(gè)元素的重綁定,所以沒(méi)有新的名字被定義。因此在函數(shù)中成功更新了全局作用于中l(wèi)所引用對(duì)象的值。
注意,下面的示例跟上面的是不一樣的:
[示例4]
def low_scope(): l = [2, 2] l = [1, 2] low_scope() print(l) # [1, 2]
我們可以用本節(jié)中示例1的方法解釋它。
綜上,可以認(rèn)為:
自由變量可讀不可寫(xiě)。
3.2 global和nonlocal
總是存在打破規(guī)則的需求:
在低層作用域中需要重綁定高層作用域名字,即通過(guò)自由名字重綁定。
于是global語(yǔ)句和nonlocal語(yǔ)句因運(yùn)而生。
global_stmt ::= "global" identifier ("," identifier)*
The global statement is a declaration which holds for the entire current code block. It means that the listed identifiers are to be interpreted as globals. It would be impossible to assign to a global variable without global, although free variables may refer to globals without being declared global.
global語(yǔ)句是適用于當(dāng)前代碼塊的聲明語(yǔ)句。列出的標(biāo)識(shí)符被解釋為全局名字。雖然自由名字可以不被聲明為global就能引用全局名字,但是不使用global關(guān)鍵字綁定全局名字是不可能的。
nonlocal_stmt ::= "nonlocal" identifier ("," identifier)*
The nonlocal statement causes the listed identifiers to refer to previously bound variables in the nearest enclosing scope excluding globals. This is important because the default behavior for binding is to search the local namespace first. The statement allows encapsulated code to rebind variables outside of the local scope besides the global (module) scope.
nonlocal語(yǔ)句使得列出的名字指向最近封閉函數(shù)中綁定的名字,而不是全局名字。默認(rèn)的綁定行為會(huì)首先搜索局部作用域。nonlocal語(yǔ)句使得在內(nèi)層函數(shù)中重綁定外層函數(shù)作用域中的名字成為可能,即使同名的名字存在于全局作用域。
經(jīng)典的官方示例:
def scope_test():
def do_local():
spam = 'local spam'
def do_nonlocal():
nonlocal spam # 當(dāng)外層作用域不存在spam名字時(shí),nonlocal不能像global那樣自作主張定義一個(gè)
spam = 'nonlocal spam' # 自由名字spam經(jīng)nonlocal聲明后,可以做重綁定操作了,可寫(xiě)的。
def do_global():
global spam # 即使全局作用域中沒(méi)有名字spam的定義,這個(gè)語(yǔ)句也能在全局作用域定義名字spam
spam = 'global spam' # 自有變量spam經(jīng)global聲明后,可以做重綁定操作了,可寫(xiě)的。
spam = 'test spam'
do_local()
print("After local assignment:", spam) # After local assignment: test spam
do_nonlocal()
print("After nonlocal assignment:", spam) # After nonlocal assignment: nonlocal spam
do_global()
print("After global assignment:", spam) # After global assignment: nonlocal spam
scope_test()
print("In global scope:", spam) # In global scope: global spam
作者說(shuō)不行nonlocal的邪:
def nest_outter():
spam = 'outer'
def nest_inner():
nonlocal spam1
spam1 = 'inner'
nest_inner()
print(spam)
nest_outter()
Output:
File "scope_test.py", line 41 nonlocal spam1 SyntaxError: no binding for nonlocal 'spam1' found
4. 一些坑
作者曾經(jīng)自信滿滿認(rèn)為透徹了解了Python的作用域,但是一大堆坑踩得觸不及防。
4.1 坑1 - UnboundLocalError
def test(): print(i) i = 1 i = 2 test()
Output:
Traceback (most recent call last): File "scope_test.py", line 42, in <module> test() File "scope_test.py", line 38, in test print(i) UnboundLocalError: local variable 'i' referenced before assignment
其實(shí)忽略掉全局作用域中i = 2這條語(yǔ)句,都可以理解。
Usually, the local scope references the local names of the (textually) current function.
Python對(duì)局部作用域情有獨(dú)鐘,解釋器執(zhí)行到print(i),i在局部作用域沒(méi)有。解釋器嘗試?yán)^續(xù)執(zhí)行后面定義了名字i,解釋器就認(rèn)為代碼在定義之前就是用了名字,所以拋出了這個(gè)異常。如果解釋器解釋完整個(gè)函數(shù)都沒(méi)有找到名字i,那就會(huì)沿著搜索鏈LEGB往上找了,最后找不到拋出NameError異常。
4.2 坑2 - 類的局部作用域
class Test(object):
i = 1
def test_print(self):
print(i)
t = Test()
i = 2
t.test_print()
我就問(wèn)問(wèn)大家,這個(gè)輸出什么?
當(dāng)然會(huì)出乎意料輸出2了,特別是有其他語(yǔ)言經(jīng)驗(yàn)的人會(huì)更加困惑。
上文強(qiáng)調(diào)過(guò):
函數(shù)命名空間的生命周期是什么? 調(diào)用開(kāi)始,返回或者異常結(jié)束,雖然示例中是調(diào)用的方法,但其本質(zhì)是調(diào)用類的函數(shù)。
類命名空間的作用域是什么?類定義開(kāi)始,類完成定義結(jié)束。
類定義開(kāi)始時(shí),創(chuàng)建新的屬于類的命名空間,用作局部作用域。類定義完后,命名空間銷(xiāo)毀,沒(méi)有直接方法訪問(wèn)到類中的i了(除非通過(guò)間接訪問(wèn)的方式:Test.i)。
方法調(diào)用的本質(zhì)是函數(shù)調(diào)用:
class Test(object):
i = 1
def test_print(self):
print(i)
t = Test()
i = 2
# t.test_print()
Test.test_print(t) # 方法調(diào)用最后轉(zhuǎn)換成函數(shù)調(diào)用的方式
函數(shù)調(diào)用開(kāi)始,其作用域與全局作用域有了上下級(jí)關(guān)系(L和G),函數(shù)中i作為自由名字,最后輸出2。
因此,不能被類中數(shù)據(jù)成員和函數(shù)成員的位置迷惑,始終切記,Python中兩種訪問(wèn)引用的方式:
直接引用:試圖直接寫(xiě)名字name引用名字,Python按照搜索LEGB作用域的方式搜索名字。
間接引用:使用objname.attrname的方式引用名字attrname,Python不搜索作用域,直接去對(duì)象里找屬性。
4.3 坑3 - 列表推導(dǎo)式的局部作用域
一個(gè)正常列表推導(dǎo)式:
a = 1 b = [a + i for i in range(10)] print(b) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
現(xiàn)在把列表推導(dǎo)式放到類中:
class Test(object):
a = 1
b = [a + i for i in range(10)]
print(b)
def test(self):
pass
Output:
Traceback (most recent call last): File "scope_test.py", line 15, in <module> class Test(object): File "scope_test.py", line 18, in Test b = [a + i for i in range(10)] File "scope_test.py", line 18, in <listcomp> b = [a + i for i in range(10)] NameError: name 'a' is not defined
輸出反饋名字a未定義。
上文強(qiáng)調(diào)過(guò),解釋器讀取類定義開(kāi)始class ClassName后,創(chuàng)建命名空間用作局部作用域。
語(yǔ)句a = 1,在這個(gè)局部作用域中定義了名字i
語(yǔ)句b = [a + i for i in rage(10)],列表推導(dǎo)式同樣創(chuàng)建了一個(gè)局部作用域。這個(gè)作用域與類定義的局部作用域并沒(méi)有上下級(jí)關(guān)系,所以,自然沒(méi)有任何直接訪問(wèn)名字a的方法。
Python中只有四種作用域:LEGB,因?yàn)轭惗x的局部作用域與列表推導(dǎo)式的局部作用域于不是嵌套函數(shù)關(guān)系,所以并不能構(gòu)成Enclosing作用域關(guān)系。因此它們是兩個(gè)獨(dú)立的局部作用域,不能相互訪問(wèn)。
既然是兩個(gè)獨(dú)立局部作用域,那么上述例子就等同于:
def test1(): i = 1 def test2(): print(i) test1() test2()
期待在test2中訪問(wèn)test1的名字i,顯然是不可行的。
以上這篇Python進(jìn)階_關(guān)于命名空間與作用域(詳解)就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
在 Python 中接管鍵盤(pán)中斷信號(hào)的實(shí)現(xiàn)方法
要使用信號(hào),我們需用導(dǎo)入 Python 的signal庫(kù)。然后自定義一個(gè)信號(hào)回調(diào)函數(shù),當(dāng) Python 收到某個(gè)信號(hào)時(shí),調(diào)用這個(gè)函數(shù)。 ,下面通過(guò)實(shí)例代碼給大家介紹在 Python 中接管鍵盤(pán)中斷信號(hào),需要的朋友可以參考下2020-02-02
Python實(shí)現(xiàn)統(tǒng)計(jì)文本中字符的方法小結(jié)
在計(jì)算機(jī)編程中,經(jīng)常需要對(duì)文本數(shù)據(jù)進(jìn)行處理和分析,字符統(tǒng)計(jì)是其中一個(gè)常見(jiàn)任務(wù),本文將詳細(xì)介紹如何使用Python進(jìn)行字符統(tǒng)計(jì),希望對(duì)大家有所幫助2024-01-01
python?Copula?實(shí)現(xiàn)繪制散點(diǎn)模型
這篇文章主要介紹了python?Copula實(shí)現(xiàn)繪制散點(diǎn)模型,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07
Pytest單元測(cè)試框架生成HTML測(cè)試報(bào)告及優(yōu)化的步驟
本文主要介紹了Pytest單元測(cè)試框架生成HTML測(cè)試報(bào)告及優(yōu)化的步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
Python如何使用正則表達(dá)式分割字符串舉4例說(shuō)明
在Python編程中我們經(jīng)常需要對(duì)字符串進(jìn)行分割操作,下面這篇文章主要給大家介紹了關(guān)于Python如何使用正則表達(dá)式分割字符串的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-05-05
Django微信小程序后臺(tái)開(kāi)發(fā)教程的實(shí)現(xiàn)
這篇文章主要介紹了Django微信小程序后臺(tái)開(kāi)發(fā)教程的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
python處理“”開(kāi)頭加數(shù)字的html字符方法
在本篇文章里小編給大家整理了關(guān)于python如何處理“&#”開(kāi)頭加數(shù)字的html字符的相關(guān)知識(shí)點(diǎn)總結(jié),有興趣的朋友們學(xué)習(xí)下。2019-06-06

