Python 中名稱的有效範圍 (scope)
codemee
Posted on January 5, 2023
在 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
結語
本文希望透過簡短的文章與圖解, 讓初學者可以分清楚程式中實際使用的名稱到底是哪一個?避免因為用到尚未綁定的名稱、或是用錯名稱導致程式錯誤, 實際上可能還有一些細節, 不過對於一般程式來說, 本文提到的部分應該已經夠用了。
Posted on January 5, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.