作用域與名稱空間的坑
1. 名稱空間
1.1 什麼是名稱空間
Namespace名稱空間,也稱名字空間,是從名字到物件的對映。Python中,大部分的名稱空間都是由字典來實現的,但是本文的不會涉及名稱空間的實現。名稱空間的一大作用是避免名字衝突:
?1 2 3 4 5 |
def
fun1():
i
=
1
def
fun2():
i
=
2
|
同一個模組中的兩個函式中,兩個同名名字i之間絕沒有任何關係,因為它們分屬於不同明明空間。
1.2 名稱空間的種類
常見的名稱空間有:
built-in名字集合,包括像abs()這樣的函式,以及內建的異常名字等。通常,使用內建這個詞表示這個名稱空間-內建名稱空間
模組全域性名字集合,直接定義在模組中的名字,如類,函式,匯入的其他模組等。通常,使用全域性名稱空間表示。
函式呼叫過程中的名字集合,函式中的引數,函式體定義的名字等,在函式呼叫時被“啟用”,構成了一個名稱空間。通常,使用區域性名稱空間表示。
一個物件的屬性集合,也構成了一個名稱空間。但通常使用objname.attrname的間接方式訪問屬性,而不是直接訪問,故不將其列入名稱空間討論。
類定義的名稱空間,通常直譯器進入類定義時,即執行到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
|