懶得掃全文的童鞋,可以直接跳到最後看總結。
我們先從一個簡單的栗子說起:
栗子
a 檔案中有變數 va
以及類 A
,b 檔案匯入 a
中class A
,並列印出 A
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#a.py va = ['dobi', 'a', 'dog'] print('a1', id(va)) class A(): def __init__(self): pass def rtn(self): global va va.insert(1,'is') print('a3', id(va)) return va print('a2', va) #b.py from a import A print('b', A) |
執行 b 檔案的結果為:
1 2 3 4 |
Reloaded modules: a a1 2407907960200 a2 ['dobi', 'a', 'dog'] b |
可以發現,雖然 b 只是匯入了 a 中的 class A
,但匯入這個過程卻執行了整個 a 檔案,那麼我們是否能夠在 b 中訪問 a 中的全域性變數 va
呢:
1 2 3 4 5 6 |
print(va) # NameError: name 'va' is not defined print(a.va) # NameError: name 'a' is not defined print(b.va) # NameError: name 'b' is not defined |
嘗試了各類呼叫方法,發現都無法正常訪問 a 的全域性變數 va
,既然 b 的匯入執行了整個 a 檔案,甚至還列印出了 va
的 id
和值,又為什麼無法在 b 中呼叫 va
呢?
這個問題所涉及到的內容就是:名稱空間。
但在開始正題之前,我們需要闡明若干概念:
一些基本概念的澄清
物件
Python 一切皆物件,每個物件都具有 一個ID、一個型別、一個值;物件一旦建立,ID 便不會改變,可以直觀的認為 ID 就是物件在記憶體中的地址:
1 2 3 4 5 6 7 8 9 |
a = [1, 2] b = a id(a) # 2407907978632 id(b) # 2407907978632 b[1] = 3 a # [1, 3] |
上例 a, b 共享了同一個 ID、同一個值、同一個型別。因此 a, b 表達的是同一個物件,但 a, b 又明顯是不同的,比如一個叫 'a'
一個叫 'b'
…既然是同一個物件,為什麼又有不同的名字呢?難道名字不是物件的屬性?
識別符號
事實確實如此,這是 Python 比較特殊一點:如同'a'
'b'
這樣的名稱其實有一個共同的名字:identifier(注意不要與 ID 混淆了),中文名為“識別符號”,來解釋一下:
識別符號:各類物件的名稱,比如函式名、方法名、類名,變數名、常量名等。
在 Python 中賦值並不會直接複製資料,而只是將名稱繫結到物件,物件本身是不知道也不需要關心(該關心這個的是程式猿)自己叫什麼名字的。一個物件甚至可以指向不同的識別符號,上例中的'a'
'b'
便是如此。真正管理這些名子的事物就是本文的主角”名稱空間” 。
名稱空間
名稱空間:(英語:Namespace)表示識別符號(identifier)的可見範圍。(ps:copy 自 SF)
簡而言之,名稱空間可以被簡單的理解為:存放和使用物件名字的抽象空間。
作用域
與名稱空間相對的一個概念就是“作用域”,那麼什麼又是作用域呢?
作用域:(英文 Scope)是可以直接訪問到名稱空間的文字區域。
這裡需要搞清楚什麼是直接訪問:
1 2 3 4 |
#x.py a = 1 class A(): def func():pass |
1 2 3 4 |
python x.py a #直接訪問 # 1 A.func #屬性訪問 |
Python 中不加 .
的訪問為直接訪問,反之為屬性訪問。
因此作用域必定是相對某個物件內部而言的,比如一個函式內部、一個模組全域性等,那作用域和名稱空間是什麼關係呢:
- 作用域是一種特殊的名稱空間,該空間內的名稱可以被直接訪問;
- 並不是所有的名稱空間都是作用域。
看不懂? 沒關係,後面會解釋,我們先回到名稱空間這個話題上:
現今 Python 的大部分名稱空間是通過字典來實現的,也即一個名稱空間就是名字到物件的對映。另外, Python 允許對名稱空間進行巢狀,而我們經常接觸的名稱空間有四層:
LEGB 規則
LEGB 層級
這四層名稱空間可以簡記為 LEGB:
- 區域性名稱空間(local):指的是一個函式所定義的空間或者一個類的所有屬性所在的空間,但需注意的是函式的區域性名稱空間是一個作用域,而類的區域性名稱空間不是作用域。
- 閉包名稱空間(enclosing function):閉包函式 的作用域(Python 3 引入)。
- 全域性名稱空間(global):讀入一個模組(也即一個.py文件)後產生的作用域。
- 內建名稱空間(builtin):Python 直譯器啟動時自動載入__built__模組後所形成的名字空間;諸如 str/list/dict…等內建物件的名稱就處於這裡。
為了說清楚這幾層洋蔥皮,舉個例子:
1 2 3 4 5 6 7 8 9 |
#c.py v1 = 'a global var' def func(v): v2 = 'a local var' def inn_func(): v3 = v2 + v return v3 return inn_func |
內建名稱空間比較好理解,我們重點講解下其他三個:
'v1'
為全域性變數v1
的名子,其所處的名稱空間為全域性名稱空間;需要注意的是全域性名稱空間包括'func'
但不包括func
的作用域。func
內部囊括'v2'
和'inn_func'
名稱的空間為區域性名稱空間;- 執行
func
後,func
的作用域釋放(或許遺忘更合適),並返回了繫結了v
和v2
變數的閉包函式inn_func
,此時閉包所具有的作用域為閉包空間,因此區域性名稱空間和閉包名稱空間是相對而言的,對於父函式func
而言,兩者具有產生時間上的差異。
LEGB 訪問規則
搞清楚各個層級概念後,我們來說一下 LEGB 的訪問規則: 同樣的識別符號在各層名稱空間中可以被重複使用而不會發生衝突,但 Python 尋找一個識別符號的過程總是從當前層開始逐層往上找,直到首次找到這個識別符號為止:
1 2 3 4 5 6 7 8 9 10 |
#d.py v1 = 1 v2 = 3 def f(): v1 = 2 print(1, v1) print(2, v2) f() print(3, v1) |
1 2 3 |
1 2 2 3 3 1 |
上例中,全域性變數和函式 f
都定義了 變數 v1
,結果 Python 會優先選擇 f
的區域性變數 v1
,對於 f
內並未定義的變數 v2
,Python 會向上搜尋全域性名稱空間,讀取全域性變數 v2
後列印輸出。
global 和 nonlocal 語句
global 和 nonlocal 的作用
如前所述,對於上層變數,python 允許直接讀取,但是卻不可以在內層作用域直接改寫上層變數,來看一個典型的閉包結構:
1 2 3 4 5 6 7 8 9 10 11 |
#e.py gv = ['a', 'global', 'var'] def func(v): gv = ['gv'] + gv #UnboundLocalError:local variable 'gv' referenced before assignment lv = [] def inn_func(): lv = lv + [v] #UnboundLocalError:local variable 'lv' referenced before assignment gv.insert(1, lv[0]) return gv return inn_func |
上面對函式內的 gv
和 lv
進行賦值操作後,兩處都會發生 UnboundLocalError
,因為 Python 並不知道你是想在內層作用域生成一個同名的區域性變數,還是想直接改寫上層變數,因此便會引起錯誤。為此,Python 引入了 global
、nonlocal
語句就來說明所修飾的 gv
、lv
分別來自全域性作用域和父函式作用域,宣告之後,就可以在 func
和 inn_func
內直接改寫上層作用域內 gv
和 lv
的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#f.py gv = ['a', 'global', 'var'] def func(v): global gv gv = ['gv'] + gv lv = [] print(id(lv)) def inn_func(): nonlocal lv lv = lv + [v] print(id(lv)) gv.insert(1, lv[0]) return gv return inn_func |
1 2 3 4 5 6 7 8 9 |
a = func('is') # 2608229974344 a() # 2608229974344 # ['gv', 'is', 'a', 'global', 'var'] print(gv) # ['gv', 'is', 'a', 'global', 'var'] |
如上,全域性變數 gv
值被函式改寫了, inn_func
修改的也確實是父函式 lv
的值 (依據 ID 判斷)。
借殼
那麼是不是不使用 global
和 nonlocal
就不能達到上面的目的呢?來看看這段程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#g.py gv = ['a', 'global', 'var'] def func(v): gv.insert(0, 'gv') lv = [] print(id(lv)) def inn_func(): lv.append(v) print(id(lv)) gv.insert(1, lv[0]) return gv return inn_func |
執行的結果:
1 2 3 4 5 6 7 8 9 |
a = func('is') # 2608110869168 a() # 2608110869168 # ['gv', 'is', 'a', 'global', 'var'] print(gv) # ['gv', 'is', 'a', 'global', 'var'] |
可以發現,執行結果同上面完全一致,問題自然來了:“為什麼不用 global
nonlocal
也可以改寫全域性變數gv
和父函式變數lv
的值?
為了看清楚這個過程,我們將上面的gv.insert(0, 'gv')
lv.append(v)
改寫為 gv[0:0] = ['gv']
lv[:] = [v]
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#h.py gv = ['a', 'global', 'var'] def func(v): gv[0:0] = ['gv'] lv = [] print(id(lv)) def inn_func(): lv[:] = [v] print(id(lv)) gv.insert(1, lv[0]) return gv return inn_func |
執行結果:
1 2 3 4 5 6 |
a = func('is') # 2608229959496 a() # 2608229959496 # ['gv', 'is', 'a', 'global', 'var'] |
同 g.py 檔案的執行結果完全一致,事實上兩者之間的內在也是完全一樣的。
So 我們其實改寫的不是 gv
和 lv
,而是 gv
和 lv
的元素 gv[0:0]
和 lv[:]
。因此,不需要 global
和 nonlocal
修飾就可以直接改寫,這就是“借殼”,在 nonlocal
尚未引入 Python 中,比如 Python 2.x 若要在子函式中改寫父函式變數的值就得通過這種方法。
當然借殼蘊藏著一個相對複雜的識別符號建立的問題:比如子函式通過借殼修改父函式變數lv
的值,那麼子函式的識別符號lv
是怎麼繫結到父函式變數lv
的值 ID 的上的?
關於這個問題,這裡有個問答就是討論這個的:python的巢狀函式中區域性作用域問題?
global 和 nonlocal 語句對識別符號建立的不同影響
另外,需要注意的是:global
語句只是宣告該識別符號引用的變數來自於全域性變數,但並不能直接在當前層建立該識別符號;nonlocal
語句則會在子函式名稱空間中建立與父函式變數同名的識別符號:
1 2 3 4 5 6 7 8 9 10 11 12 |
#j.py gv = 'a global var' def func(): global gv lv = 'a local var' print(locals()) def inn_func(): nonlocal lv global gv print(locals()) return inn_func |
執行結果:
1 2 3 4 5 |
c = func() {'lv': 'a local var'} #執行 `func` 函式後,`global` 語句並未將 `gv` 變數引入區域性名稱空間 c() {'lv': 'a local var'} #執行閉包函式後,`nonlocal` 語句將父函式變數 `lv` 引入閉包名稱空間 |
之所以 nonlocal
語句與 global
語句的處置不同,在於全域性變數在模組內隨時都可以訪問,而父函式變數在父函式執行完畢後便會釋放,因此 nonlocal
語句必須將父函式變數的識別符號和引用寫入閉包名稱空間。
名稱空間和識別符號的建立
建立規則
實際上,到這裡其實還有一個重要的重要問題沒有解決:“識別符號並不是天生就在名稱空間內的,名稱空間也不是平白無故就產生的,那麼什麼時候會建立名稱空間和識別符號呢?”
規則有三:
- 賦值、定義函式和類時產生識別符號;
- 類和函式定義(def 和 lambda)執行時產生新的名稱空間;
- 識別符號產生地點決定識別符號所處的名稱空間。
這三點就是拿來秒懂的!不過,仍然有一點常常被忽視:類的名稱空間:
類的區域性名稱空間
首先,函式和類執行時都會產生區域性名稱空間,但類的執行機制不同於函式:
1 2 3 4 5 6 7 8 9 10 |
#i.py def a(): print('function') class A(): print(1) class B(): print(2) class C(): print(3) |
執行檔案,結果為:
1 2 3 |
1 2 3 |
如上,類就是一個可執行的程式碼塊,只要該類被載入,就會被執行,這一點不同於函式。
類之所以這麼設計的原因在於:類是建立其他例項(生成其他的類或者具體的物件)的物件,因此必須在例項之前被建立,而類又可能涉及到與其他類的繼承、過載等一系列問題,故在程式碼載入時就被建立利於提高效率和降低邏輯複雜度。
其次,與函式不同的是,類的區域性名稱空間並非作用域
1 2 3 |
class A(): a = 1 b = [a + i for i in range(3)] #NameError: name 'a' is not defined |
執行上段程式碼,我們可以發現在類 A
內列表推導式無法調取 a
的值,但函式卻可以:
1 2 3 4 5 6 |
def func(): a = 1 b = [a + i for i in range(3)] print(b) func() #[1, 2, 3] |
因此,A
中的 a
不同於函式 func
中的 a
在區域性名稱空間中可以被任意讀取,之所以說是“不可以被任意”讀取而不是“不可被讀取”,原因在於在類A
的區域性空間內,a
其實一定程度上是可以直接被讀取的:
1 2 3 |
class A(): a = 1 c = a + 2 |
執行上段程式碼後:
1 2 |
A.c #3 |
而上例中 b
的賦值操作不能執行,原因在於列表推導式會建立自己的區域性名稱空間,因此難以訪問到 a
。
編譯與區域性名稱空間
Python 是動態語言,很多行為是動態發生的,但 Python 自身也在不斷進步,比如為了提高效率,有些行為會在編譯時候完成,區域性變數的建立就是如此:
1 2 3 4 5 6 |
def func(): a = 1 def inn_func(): print(a) # error a = 2 # error inn_func() |
上段程式還未執行,就提示存在有語法錯誤,原因在於python 直譯器發現 inn_func
記憶體在自身的 a
變數,但卻在宣告之前就被 print
了。
總結
囉嗦了這麼多,終於該結尾了!
我們再來回過頭來看下文章開頭的栗子:
1、為什麼 b.py 只是匯入 a.py 中的 class A
,卻執行了整個 a.py 檔案?
答:因為 Python 並不知道 class A
在 a.py 文件的何處,為了能夠找到 class A
,Python 需要執行整個文件。
2、為什麼 b.py 的匯入執行了整個 a.py 文件,卻在 b 中難以呼叫 a 的全域性變數 va
?
答:Python 的全域性變數指的是模組全域性,因此不可以跨文件,因此 global 語句也是不可以跨文件的。另外, b 只是匯入了 a 的 class A
,因此並不會匯入 a 中所有的識別符號,所以 類似a.va
這樣的呼叫也是不起作用的。
關於名稱空間:
1、賦值、定義類和函式都會產生新的識別符號;
2、全域性變數的識別符號不能跨文件;
3、各級名稱空間相互獨立互不影響;
4、Python 總是從當前層逐漸向上尋找識別符號;
5、內層作用域若想直接修改上層變數,需要通過 global
nonlocal
語句先宣告;
6、單純的 global
語句並不能為所在層級建立相應識別符號,但 nonlocal
語句可以在閉包空間中建立相應識別符號;
7、類的區域性名稱空間不是作用域。