做用域與命名空間的坑

1. 命名空間html

1.1 什麼是命名空間python

Namespace命名空間,也稱名字空間,是從名字到對象的映射。Python中,大部分的命名空間都是由字典來實現的,可是本文的不會涉及命名空間的實現。命名空間的一大做用是避免名字衝突:程序員

?
1
2
3
4
5
def fun1():
   i = 1
 
def fun2():
   i = 2

同一個模塊中的兩個函數中,兩個同名名字i之間絕沒有任何關係,由於它們分屬於不一樣明明空間。編程

1.2 命名空間的種類編程語言

常見的命名空間有:ide

built-in名字集合,包括像abs()這樣的函數,以及內置的異常名字等。一般,使用內置這個詞表示這個命名空間-內置命名空間函數

模塊全局名字集合,直接定義在模塊中的名字,如類,函數,導入的其餘模塊等。一般,使用全局命名空間表示。ui

函數調用過程當中的名字集合,函數中的參數,函數體定義的名字等,在函數調用時被「激活」,構成了一個命名空間。一般,使用局部命名空間表示。spa

一個對象的屬性集合,也構成了一個命名空間。但一般使用objname.attrname的間接方式訪問屬性,而不是直接訪問,故不將其列入命名空間討論。.net

類定義的命名空間,一般解釋器進入類定義時,即執行到class ClassName:語句,會新建一個命名空間。(見官方對類定義的說明)

1.3 命名空間的生命週期

不一樣類型的命名空間有不一樣的生命週期:

內置命名空間,在Python解釋器啓動時建立,解釋器退出時銷燬;

全局命名空間,模塊的全局命名空間在模塊定義被解釋器讀入時建立,解釋器退出時銷燬;

局部命名空間,這裏要區分函數以及類定義。函數的局部命名空間,在函數調用時建立,函數返回或者由未捕獲的異常時銷燬;類定義的命名空間,在解釋器讀到類定義建立,類定義結束後銷燬。(關於類定義的命名空間,在類定義結束後銷燬,但其實類對象就是這個命名空間內容的包裝,見官方對類定義的說明)

2. 做用域

2.1 什麼是做用域

做用域是Python的一塊文本區域,這個區域中,命名空間能夠被「直接訪問」。這裏的直接訪問指的是試圖在命名空間中找到名字的絕對引用(非限定引用)。這裏有必要解釋下直接引用和間接引用:

直接引用;直接使用名字訪問的方式,如name,這種方式嘗試在名字空間中搜索名字name。

間接引用;使用形如objname.attrname的方式,即屬性引用,這種方式不會在命名空間中搜索名字attrname,而是搜索名字objname,再訪問其屬性。

2.2 與命名空間的關係

如今,命名空間持有了名字。做用域是Python的一塊文本區域,即一塊代碼區域,須要代碼區域引用名字(訪問變量),那麼必然做用域與命名空間之間就有了聯繫。

顧名思義,名字做用域就是名字能夠影響到的代碼文本區域,命名空間的做用域就是這個命名空間能夠影響到的代碼文本區域。那麼也存在這樣一個代碼文本區域,多個命名空間能夠影響到它。
做用域只是文本區域,其定義是靜態的;而名字空間倒是動態的,只有隨着解釋器的執行,命名空間纔會產生。那麼,在靜態的做用域中訪問動態命名空間中的名字,形成了做用域使用的動態性。

那麼,能夠這樣認爲:

靜態的做用域,是一個或多個命名空間按照必定規則疊加影響代碼區域;運行時動態的做用域,是按照特定層次組合起來的命名空間。

在必定程度上,能夠認爲動態的做用域就是命名空間。在後面的表述中,我會把動態的做用域與其對應命名空間等同起來。

2.3 名字搜索規則

在程序中引用了一個名字,Python是怎樣搜索到這個名字呢?

在程序運行時,至少存在三個命名空間能夠被直接訪問的做用域:

Local
首先搜索,包含局部名字的最內層(innermost)做用域,如函數/方法/類的內部局部做用域;

Enclosing
根據嵌套層次從內到外搜索,包含非局部(nonlocal)非全局(nonglobal)名字的任意封閉函數的做用域。如兩個嵌套的函數,內層函數的做用域是局部做用域,外層函數做用域就是內層函數的 Enclosing做用域;

Global
倒數第二次被搜索,包含當前模塊全局名字的做用域;

Built-in
最後被搜索,包含內建名字的最外層做用域。

程序運行時,LGB三個做用域是必定存在的,E做用域不必定存在;若程序是這樣的:

?
1
2
i = 1
print (i)

局部做用域在哪裏呢?咱們認爲(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.

通常地,局部做用域引用函數中定義的名字。函數以外,局部做用域和全局做用域引用同一個命名空間:模塊的明星空間。然而類型的局部做用域引用了類定義新的命名空間。

Python按照以上L-E-G-B的順序依次在四個做用域搜索名字。沒有搜索到時,Python拋出NameError異常。

2.4 什麼時候引入做用域咱們知道:

咱們知道:

在Python中一個名字只有在定義以後,才能引用。

?
1
print (i)

直接引用未定義的名字i,按照搜索規則,在LGB三個做用域均沒有搜索到名字i(LB相同命名空間)。拋出NameError異常:

?
1
2
3
4
Traceback (most recent call last):
  File "scope_test.py" , line 15 , in <module>
   print (i)
NameError: name 'i' is not defined

那對於這段代碼呢?

?
1
2
3
4
5
6
def try_to_define_name():
   '''函數中定義了名字i,並綁定了一個整數對象1'''
   i = 1
 
try_to_define_name()
print (i) #引用名字i以前,調用了函數

在引用名字i以前,明明調用了函數,定義了名字i,但是仍是找不到這個名字:

?
1
2
3
4
Traceback (most recent call last):
  File "scope_test.py" , line 20 , in <module>
   print (i) #引用名字i以前,調用了函數
NameError: name 'i' is not defined

雖然定義了名字i,可是定義在了函數的局部做用域對應的局部命名空間中,按照LEGB搜索規則,在全局做用域中天然訪問不到局部做用域;再者,函數調用結束後,這個命名空間被銷燬了。

引用名字老是與做用域相關的,所以:

在Python中一個名字只有在定義以後,才能在合適的做用域引用。

那麼,在定義名字時,就要注意名字定義的做用域了,以避免定義後須要訪問時卻找不到。因此,瞭解Python在什麼時候會引入新的做用域頗有必要。通常來講,B,G兩個做用域的引入在不可以經過代碼操做的,可以經過語句引入的做用域只有E,L了。Python中引入新做用域的語句頗有限,總的來講只有兩類一個:

函數定義引入local做用域或者Enclosing做用域;本質上,lambda和生成器表達式也是函數,會引入新做用域。

類定義引入local做用域;

列表推導式引入local做用域,傳說在python2中列表推導式不引入新的做用域

幾個會讓有其餘高級語言經驗的猿困惑的地方:

if語句:

?
1
2
3
if True :
   i = 1
print (i) # output: 1,而不是NameError

if語句並不會引入新的做用域,因此名字綁定語句i = 1與print(i)是在同一個做用域中。

for語句:

?
1
2
3
for i in range ( 6 ):
   pass
print (i) #output: 5,而不是NameError

for語句一樣不會引入新的做用域,因此名字i的綁定和重綁定與print(i)在同一個做用域。這一點Python就比較坑了,所以寫代碼時切忌for循環名字要與其餘名字不重名才行。

import語句:

?
1
2
3
4
5
6
def import_sys():
   '''import sys module'''
   import sys
 
import_sys()
print (sys.path) # NameError: name 'sys' is not defined

這個算非正常程序員的寫法了,import語句在函數import_sys中將名字sys和對應模塊綁定,那sys這個名字仍是定義在局部做用域,跟上面的例子沒有任務區別。要時刻切記Python的名字,對象,這個其餘編程語言不同,可是:

打破第一編程語言認知的第二門編程語言,纔是值得去學的好語言。

3. 做用域應用

3.1 自由變量可讀不可寫

我不太想用「變量」這個詞形容名字,奈何變量是家喻戶曉了,Python中的自由變量:

?
1
If a variable is used in a code block but not defined there, it is a free variable.

若是引用發生的代碼塊不是其定義的地方,它就是一個自由變量。專業一點,就是:

引用名字的做用域中沒有這個名字,那這個名字就是自由名字

Note: 「自由名字」只是做者YY的,並沒獲得普遍承認。

咱們已經瞭解了做用域有LEGB的層次,並按順序搜索名字。按照搜索順序,當低層做用域不存在待搜索名字時,引用高層做用域存在的名字,也就是自由名字:

[示例1]

?
1
2
3
4
5
def low_scope():
   print(s)
 
s = 'upper scope'
low_scope()

很清楚,這段代碼的輸出是upper scope。

[示例2]

?
1
2
3
4
5
6
def low_scope():
   s = 'lower scope'
 
s = 'upper scope'
low_scope()
print(s)

很遺憾,最後的打印語句沒有按照期待打印出lower scope而是打印了upper scope。

?
1
A special quirk of Python is that – if no global statement is in effect – assignments to names always go into the innermost scope.

Python的一個怪癖是,若是沒有使用global語句,對名字的賦值語句一般會影響最內層做用域。
即賦值語句影響局部做用域,賦值語句帶來的影響是綁定或重綁定,可是在當前局部做用域的命名空間中,並無s這個名字,所以賦值語句在局部做用於定義了同名名字s,這與外層做用域中的s並不衝突,由於它們分屬不一樣命名空間。
這樣,全局做用域的s沒有被重綁定,結果就很好解釋了。

當涉及可變對象時,狀況又有所不一樣了:

[示例3]

?
1
2
3
4
5
6
def low_scope():
   l[0] = 2
 
l = [1, 2]
low_scope()
print(l) # [2, 2]

很遺憾,最後的打印語句沒有按照期待輸出[1, 2]而是輸出了[2, 2]。
上一個例子的經驗並不能運用在此,由於list做爲一個可變對象,l[0] = 2並非對名字l的重綁定,而是對l的第一個元素的重綁定,因此沒有新的名字被定義。所以在函數中成功更新了全局做用於中l所引用對象的值。

注意,下面的示例跟上面的是不同的:

[示例4]

?
1
2
3
4
5
6
def low_scope():
   l = [2, 2]
 
l = [1, 2]
low_scope()
print(l) # [1, 2]

咱們能夠用本節中示例1的方法解釋它。

綜上,能夠認爲:

自由變量可讀不可寫。

3.2 global和nonlocal

老是存在打破規則的需求:

在低層做用域中須要重綁定高層做用域名字,即經過自由名字重綁定。

因而global語句和nonlocal語句因運而生。

?
1
2
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語句是適用於當前代碼塊的聲明語句。列出的標識符被解釋爲全局名字。雖然自由名字能夠不被聲明爲global就能引用全局名字,可是不使用global關鍵字綁定全局名字是不可能的。

?
1
2
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語句使得列出的名字指向最近封閉函數中綁定的名字,而不是全局名字。默認的綁定行爲會首先搜索局部做用域。nonlocal語句使得在內層函數中重綁定外層函數做用域中的名字成爲可能,即便同名的名字存在於全局做用域。

經典的官方示例:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def scope_test():
   
   def do_local():
     spam = 'local spam'
 
   def do_nonlocal():
     nonlocal spam # 當外層做用域不存在spam名字時,nonlocal不能像global那樣自做主張定義一個
     spam = 'nonlocal spam' # 自由名字spam經nonlocal聲明後,能夠作重綁定操做了,可寫的。
 
   def do_global():
     global spam # 即便全局做用域中沒有名字spam的定義,這個語句也能在全局做用域定義名字spam
     spam = 'global spam' # 自有變量spam經global聲明後,能夠作重綁定操做了,可寫的。
 
   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

做者說不行nonlocal的邪:

?
1
2
3
4
5
6
7
8
9
10
11
def nest_outter():
   spam = 'outer'
 
   def nest_inner():
     nonlocal spam1
     spam1 = 'inner'
 
   nest_inner()
   print (spam)
 
nest_outter()

Output:

?
1
2
3
File "scope_test.py", line 41
   nonlocal spam1
SyntaxError: no binding for nonlocal 'spam1' found

4. 一些坑

做者曾經自信滿滿認爲透徹瞭解了Python的做用域,可是一大堆坑踩得觸不及防。

4.1 坑1 - UnboundLocalError

?
1
2
3
4
5
6
def test():
   print (i)
   i = 1
 
i = 2
test()

Output:

?
1
2
3
4
5
6
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

其實忽略掉全局做用域中i = 2這條語句,均可以理解。

?
1
Usually, the local scope references the local names of the (textually) current function.

Python對局部做用域情有獨鍾,解釋器執行到print(i),i在局部做用域沒有。解釋器嘗試繼續執行後面定義了名字i,解釋器就認爲代碼在定義以前就是用了名字,因此拋出了這個異常。若是解釋器解釋完整個函數都沒有找到名字i,那就會沿着搜索鏈LEGB往上找了,最後找不到拋出NameError異常。

4.2 坑2 - 類的局部做用域

?
1
2
3
4
5
6
7
8
9
10
class Test( object ):
 
   i = 1
 
   def test_print( self ):
     print (i)
 
t = Test()
i = 2
t.test_print()

我就問問你們,這個輸出什麼?
固然會出乎意料輸出2了,特別是有其餘語言經驗的人會更加困惑。

上文強調過:
函數命名空間的生命週期是什麼? 調用開始,返回或者異常結束,雖然示例中是調用的方法,但其本質是調用類的函數。
類命名空間的做用域是什麼?類定義開始,類完成定義結束。

類定義開始時,建立新的屬於類的命名空間,用做局部做用域。類定義完後,命名空間銷燬,沒有直接方法訪問到類中的i了(除非經過間接訪問的方式:Test.i)。

方法調用的本質是函數調用:

?
1
2
3
4
5
6
7
8
9
10
11
class Test( object ):
 
   i = 1
 
   def test_print( self ):
     print (i)
 
t = Test()
i = 2
# t.test_print()
Test.test_print(t) # 方法調用最後轉換成函數調用的方式

函數調用開始,其做用域與全局做用域有了上下級關係(L和G),函數中i做爲自由名字,最後輸出2。
所以,不能被類中數據成員和函數成員的位置迷惑,始終切記,Python中兩種訪問引用的方式:

直接引用:試圖直接寫名字name引用名字,Python按照搜索LEGB做用域的方式搜索名字。

間接引用:使用objname.attrname的方式引用名字attrname,Python不搜索做用域,直接去對象裏找屬性。

4.3 坑3 - 列表推導式的局部做用域

一個正常列表推導式:

?
1
2
3
a = 1
b = [a + i for i in range ( 10 )]
print (b) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

如今把列表推導式放到類中:

?
1
2
3
4
5
6
7
8
class Test( object ):
 
   a = 1
   b = [a + i for i in range ( 10 )]
   print (b)
 
   def test( self ):
     pass

Output:

?
1
2
3
4
5
6
7
8
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未定義。

上文強調過,解釋器讀取類定義開始class ClassName後,建立命名空間用做局部做用域。
語句a = 1,在這個局部做用域中定義了名字i
語句b = [a + i for i in rage(10)],列表推導式一樣建立了一個局部做用域。這個做用域與類定義的局部做用域並無上下級關係,因此,天然沒有任何直接訪問名字a的方法。

Python中只有四種做用域:LEGB,由於類定義的局部做用域與列表推導式的局部做用域於不是嵌套函數關係,因此並不能構成Enclosing做用域關係。所以它們是兩個獨立的局部做用域,不能相互訪問。

既然是兩個獨立局部做用域,那麼上述例子就等同於:

?
1
2
3
4
5
6
7
8
def test1():
   i = 1
 
def test2():
   print (i)
 
test1()
test2()

期待在test2中訪問test1的名字i,顯然是不可行的。

相關文章
相關標籤/搜索