在 Python 中程式是以程式區塊 (code block) 為單位, 每個模組 (module, 單一腳本檔)、函式本體、類別定義都是一個程式區塊, 有自己紀錄名稱與對應物件的清單, 稱為名稱空間 (namespace), 系統就是依據程式區塊間的層級關係, 找到個別名稱要對應到的物件。
綁定名稱
每當執行設定敘述、函式定義、類別定義、import 敘述, 以及傳入引數叫用函式或方法時, 就會將名稱綁定 (binding) 到對應的物件, 記錄在該程式區塊的名稱空間內。
在 Python 執行環境中, 預設會有 __builtins__ 名稱空間, 對應到 builtins 模組, 包含所有內建的名稱, 像是內建函式 print
的名稱就是記錄在這裡。當找不到綁定的名稱時, 最終就會到 __builtins__ 中尋找, 這也是我們可以不用 import 任何名稱就可以叫用 print
等內建函式的原因。
基本原則:使用在最近一層的程式區塊中綁定的名稱
當使用到某個名稱時, 基本原則就是以最靠近的程式區塊中綁定的名稱為準, 例如以下這個簡單的例子:
a = 10 def test(): b = 20 print(b) print(a) test()
一開始執行模組本身的程式區塊時, 當執行到 a = 10
後, 就會紀錄 a
名稱綁定到 10 這個整數物件;執行完 test
函式的定義後, 也會記錄 test
名稱綁定到定義好的函式:
__main__ | | a -----------> 10 | test-----------> test 函式 +-------
Python 會把執行的模組取名為 "__main__", 如果是匯入的模組, 名稱則是檔名。等到叫用 test()
時, 由於在目前的名稱空間中就可以找到 test
這個名稱, 因此會執行該名稱所綁定到的函式。這時會執行此函式的程式區塊, 並建立該區塊的命名空間, 並在執行 b = 20
後記錄 b
名稱綁定到 20 這個整數物件:
+--__main__-- | | a -----------> 10 | test-----------> 函式 | | +--test-- | | | | b---------> 20 | +-------- +------------
執行到 print(b)
時, 由於在目前的名稱空間中就可以找到 b
這個名稱, 因此印出的就是 20。接著執行 print(a)
時, 因為在目前的名稱空間中並沒有名稱 a
, 會往外層的程式區塊中尋找, 所以印出的會是上一層的 a
所綁定的 10。
最後的執行結果就會是:
# py test.py 20 10
這個搜尋名稱的動作是在執行時進行的, 因此即便把 a = 10
移到定義 test()
函式之後也沒有問題:
def test(): b = 20 print(b) print(a) a = 10 test()
實際叫用 test()
時, 已經綁定 a
了, 所以一樣可以找到名稱 a
正確執行:
# py test.py 20 10
區域 (local) 與全域 (global) 變數
在特定程式區塊內綁定的名稱, 會在程式區塊結束後跟著消失, 無法使用。舉例來說, 如果在剛剛的範例最後加上 print(b)
:
a = 10 def test(): b = 20 print(b) print(a) test() print(b)
雖然在 test
函式中有將 b
綁定到 20, 但是在 test()
叫用結束後, b
這個名稱也消失了, 執行時會因為名稱空間中找不到 b
而引發 NameError
例外:
# py test.py 20 10 Traceback (most recent call last): File "D:\code\test\test.py", line 9, in <module> print(b) NameError: name 'b' is not defined
由於所有的名稱都只在綁定時所在的程式區塊內有效, 因此稱為區域變數 (local variables)。對於在模組層級綁定的名稱, 例如前面範例中的 a
, 因為在模組內的任何地方都可以使用, 也稱它們為全域變數 (global variables), 也就是說, 模組內的名稱既是該程式區塊內的區域變數, 也是模組內的全域變數。如果使用到沒有在所在區塊內綁定的名稱, 例如前述範例中 test()
裡面的 a
, 就稱為自由變數 (free variable)。
如果在區塊中有綁定特定名稱, 就會將該名稱視為是區塊內的區域變數, 若在綁定之前就先使用該名稱, 並不會因為找不到該名稱而往外層尋找, 而是會引發 UnboundLocalError
例外, 意思就是尚未綁定的區域變數, 例如:
a = 10 def test(): b = 20 print(b) print(a) a = 30 test()
執行結果如下:
# py test.py 20 Traceback (most recent call last): File "D:\code\python\test.py", line 9, in <module> test() File "D:\code\python\test.py", line 6, in test print(a) UnboundLocalError: local variable 'a' referenced before assignment
這是因為 a
是在 print(a)
之後才綁定, 即使外層有同名的 a
也一樣。
內外層同名名稱的處理
由於是從最近一層的程式區塊開始尋找名稱, 所以若是內層與外層有同樣的名稱, 就無法使用到外層的名稱, 例如:
a = 10 def test(): a = 20 print(a) test1() def test1(): a = 30 print(a) print(a) test()
一開始執行到 print(a)
時, 找到的是模組綁定的名稱 a
:
__main__ | | a -----------> 10 | test -----------> test 函式 | test1-----------> test1 函式 +-------
因此會印出 10, 到執行 test
時, 找到的是函式內綁定的名稱 a
, 這個名稱和外層模組綁定的名稱 a
雖然同名, 但分屬於不同的名稱空間:
__main__ | | a -----------> 10 | test -----------> test 函式 | test1-----------> test1 函式 | | +--test-- | | | | a ---------> 20 | +-------- +-------
因此印出 20。到執行 test1
時, 又綁定了一個新的 a
, 如下所示:
__main__ | | a -----------> 10 | test -----------> test 函式 | test1-----------> test1 函式 | | +--test-- | | | | a ---------> 20 | +-------- | | +--test1-- | | | | a ---------> 30 | +-------- +-------
因此會印出 30。最後的執行結果如下:
# py test.py 10 20 30
請特別留意, 程式區塊的層級關係是原始碼的層級關係, 並不是函式之間叫用的關係, 也就是說, 雖然是在 test()
內叫用 test1()
, 但兩者之間並沒有包含的關係。因此, 如果我們把 test1
中綁定 a
的程式去除, 像是這樣:
a = 10 def test(): a = 20 print(a) test1() def test1(): print(a) print(a) test()
在 test1
中印出的 a
就會是外層模組中 a
綁定的 10:
# py test.py 10 20 10
但如果將 test1
定義在 test
內, 像是這樣:
a = 10 def test(): def test1(): print(a) a = 20 print(a) test1() print(a) test()
執行到 test1
的時候, 區塊的層級關係會是這樣:
__main__ | | a -----------> 10 | test -----------> test 函式 | test1-----------> test1 函式 | | +--test-- | | | | a ---------> 20 | | | | +--test1-- | | | | | +-------- | +-------- +-------
因此離 test1
最近一層就是 test
, 所以 a
名稱綁定的就是 20, 而不是模組內的 10 了:
# py test.py 10 20 20
如果把 test
中綁定名稱 a
的設定敘述去除, 像是這樣:
a = 10 def test(): def test1(): print(a) print(a) test1() print(a) test()
test1
就會再往外找到最外層的 a
, 這樣就會印出 3 個 10 了:
# py test.py 10 10 10
指定使用全域變數或是外層的區域變數
如果你想要使用的是最外層模組的全域變數 a
, 可以在 test1
中使用 global
指明要引用的全域變數, 例如:
a = 10 def test(): def test1(): global a print(a) a = 20 print(a) test1() print(a) test()
這樣系統就會知道在 test1
中使用到名稱 a
時, 要直接到最外層找, 因此列印的是最外層的 a
綁定的 10:
# py test.py 10 20 10
如果你很明確的要使用外層的區域變數, 而不是最上層模組的全域變數, 可以使用 nonlocal
, 像是這樣:
a = 10 def test(): def test1(): nonlocal a print(a) a = 20 print(a) test1() print(a) test()
這樣在 test1
中使用的就會是外層 test
中的 a
了:
# py test.py 10 20 20
nonlocal
並不只是單單往外找一層, 而是會一層層往外找, 例如:
a = 10 def test(): def test1(): def test2(): nonlocal a print(a) test2() a = 20 test1() test()
在 test2
中使用的就是往外兩層在 test
中綁定的 a
, 所以印出的是 20:
# py test.py 20
你可能會想說, 咦?這樣好像不用特別標示 nonlocal
, 不就一樣會一層層往外找尋名稱, 為什麼要多此一舉呢?這是因為 nonlocal
尋找名稱時, 並不會到最外層的模組找尋全域變數, 以底下的例子來說:
b = 10 def test(): def test1(): nonlocal b print(b) test1() test()
雖然最外層模組有名稱 b
, 可是因為在 test1
中將 b
標示為 nonlocal
, 所以尋找名稱時並不會找到最外層而出現錯誤:
# py test.py File "D:\code\test\test.py", line 5 nonlocal b ^^^^^^^^^^ SyntaxError: no binding for nonlocal 'b' found`
實際上甚至根本都還沒有執行, Python 在編譯程式碼時就發現外層區塊並沒有綁定 b
名稱, 因而引發代表語法錯誤的 SyntaxError 例外。
縮排並不會建立程式區塊
由於函式的主體需要縮排, 所以會讓人誤以為縮排也會建立一個程式區塊, 像是 C/C++ 程式用大括號建立的區塊那樣。不過事實上, 縮排並不是程式區塊, 在縮排中綁定的名稱就是隸屬於所在的程式區塊, 離開縮排區域後還是存在, 例如:
for i in range(3): a = i print(i) print(i) print(a)
在 for
迴圈結束後, 不論是隨著 for
綁定的 i
還是在 for
迴圈本體中才綁定的 a
都還是有效, 並不會消失。執行結果如下:
# py test.py 0 1 2 2 2
類別定義的程式區塊不包含類別內的方法
前面提過區塊層級是以原始碼而定, 但有個例外, 就是類別定義的程式區塊並不包含類別中的方法, 像是以下這個例子:
class A: x = 10 def test(self): print(x) a = A() a.test()
依照往最近的區塊找尋名稱的規則, 在 test
方法中找不到的 x
應該是往外層找到類定義中綁定的 x
, 不過實際上這個程式會發生錯誤:
# py test.py Traceback (most recent call last): File "D:\code\test\test.py", line 8, in <module> a.test() File "D:\code\test\test.py", line 5, in test print(x) NameError: name 'x' is not defined
這是因為實際上類別定義有它自己的名稱空間, 和類別內的方法之間是獨立的, 你可以將之視為如下:
__main__ | | a -----------> A 物件 | A -----------> 類別 A 的定義 | | +--A-- | | | | x ---------> 10 | +-------- | | +--a.test-- | | | +-------- +-------
在方法中找不到的名稱會往全域變數找, 因此如果在最外層定義 x
, a.test
就會使用最外層的 x
, 例如:
class A: x = 10 def test(self): print(x) x = 20 a = A() a.test()
執行結果如下:
# py test.py 20
或者透過 self
引用定義在類別中的 x
:
class A: x = 10 def test(self): print(self.x) a = A() a.test()
印出來的就會是 10 了:
# py test.py 10
類別定義的命名空間會成為類別自己的特徵值 (attributes), 這可以透過 object.__dict__ 查看, 例如:
>>> A.__dict__.keys() dict_keys(['__module__', 'x', 'test', '__dict__', '__weakref__', '__doc__'])
你可以看到 x
出現在其中, 我們也可以觀察 a
:
>>> a.__dict__.keys() dict_keys([])
你會發現是空的集合, 如果透過 a
引用 x
, 會因為 a
本身沒有 x
可用, 於是再透過 a.__class__
往 A
尋找而引用到類別定義中的 x
:
>>> a.x 10 >>> a.__class__ <class '__main__.A'> >>> a.__class__.x 10
如果幫 a
物件添加 x
特徵值, 那麼 a.test()
就會循 self
引用到這個 x
:
>>> a.x = 20 >>> A.x 10 >>> a.test() 20 >>> a.__dict__.keys() dict_keys(['x'])
遞迴呼叫的命名空間
前面提過, 每次執行一個程式區塊時, 就會建立一個新的名稱空間, 對於遞迴呼叫的函式, 就會建立多個同一函式的名稱空間, 以底下的例子來說:
def fact(n): if n < 2: return 1 return n * fact(n - 1) print(fact(4))
執行到 fact(4)
時的名稱空間如下:
__main__ | | fact -----------> fact 函式 | | +--fact(4)-- | | | | n ---------> 4 | +-------- +-------
但是因為遞迴, 會再依序執行 fact(3)
、fact()
、fact(1)
, 名稱空間變成:
__main__ | | fact -----------> fact 函式 | | +--fact(4)-- | | | | n ---------> 4 | +-------- | | +--fact(3)-- | | | | n ---------> 3 | +-------- | | +--fact(2)-- | | | | n ---------> 2 | +-------- | | +--fact(1)-- | | | | n ---------> 1 | +-------- +-------
也就是說, 每次叫用 fact
時, 其內的 n
都是各自專屬的名稱, 而不是所有的 fact
共用同一個 n
。這個結構會從 fact(1)
傳回 1 後依序傳回計算值, 最後得到 4*3*2*1
, 也就是 24 的值:
# py test.py 24
結語
本文希望透過簡短的文章與圖解, 讓初學者可以分清楚程式中實際使用的名稱到底是哪一個?避免因為用到尚未綁定的名稱、或是用錯名稱導致程式錯誤, 實際上可能還有一些細節, 不過對於一般程式來說, 本文提到的部分應該已經夠用了。
Top comments (0)