初探變數範圍

April 21, 2022

Python 的變數不用事先宣告,一個名稱在指定值時,就可以成為變數,並建立起自己的作用範圍(Scope)。

變數尋找/建立

在取用一個變數時,會看看目前範圍中是否有指定的變數名稱,若無則向外尋找,因此在函式中可取用全域(Global)變數:

>>> x = 10
>>> def func():
...     print(x)
...
>>> func()
10
>>>

在上面的例子中,func 中沒有區域變數 x,因此往外尋找而取得全域範圍建立的變數 x。如果在 func 中,對名稱 x 作了指定值的動作呢?

>>> x = 10
>>> def func():
...     x = 20
...     print(x)
...
>>> func()
20
>>> print(x)
10
>>>

func 中進行 x = 20 的時候,其實就建立了 func 自己的區域變數 x,而不是將全域變數 x 設為 20,因此在 func 執行完畢後,顯示全域變數 x` 的值仍會是 10。

變數範圍

就目前而言可以先知道的是,變數可以在內建(Builtin)、全域(Global)、外包函式(Endosing function)、區域函式(Local function)中尋找或建立。一個例子如下:

func  scope_demo.py
x = 10                    # 建立全域 x

def outer():
    y = 20                # 建立區域 y

    def inner():
        z = 30            # 建立區域 z
        print('x = ', x)  # 取用全域 x
        print('y = ', y)  # 取用 outer 函式的 y
        print('z = ', z)  # 取用 inner 函式的 z

    inner()

    print('x = ', x)      # 取用全域 x
    print('y = ', y)      # 取用 outer 函式的y

outer()
print('x = ', x)          # 取用全域 x 

取用名稱時(而不是對名稱指定值),一定是從最內層往外尋找。Python 中的全域,實際上是以模組檔案為界,以上例來說,x 實際上是 scope_demo 模組範圍中的變數,不會橫跨其他模組。

經常使用的 print 名稱,是屬於內建範圍,在 Python 有個 builtins 模組,該模組中的名稱範圍,橫跨各個模組。例如:

>>> import builtins
>>> dir(builtins)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError' …略

dir 函式可用來查詢指定的物件上可取用的名稱。Python 可以直接使用的函式,其名稱有在 builtins 模組定義。

global/nonlocal

Python 還有個 globals,可以取得全域變數的名稱與值,在全域範圍呼叫 locals 時,取得結果與 globals 是相同的。

如果對變數指定值時,希望是針對全域範圍的話,可以使用 global 宣告。例如:

>>> x = 10
>>> def func():
...     global x, y
...     x = 20
...     y = 30
...
>>> func()
>>> x
20
>>> y
30
>>>

來看看以下這個會發生什麼事情?

>>> x = 10
>>> def func():
...     print(x)
...     x = 20
...
>>> func()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in func
UnboundLocalError: local variable 'x' referenced before assignment
>>>

func 函式中有個 x = 20 的指定,python 直譯器會認為,print(x) 中的 xfunc 函式中的區域變數 x,因為範圍內有指定 x 的陳述句,就流程而言,在指定區域變數 x 的值之前,就要顯示其值是個錯誤。如果真的想顯示全域的 x 值,可以在 print(x) 前一行,使用 global x 宣告。

當然,無論是哪種程式語言,除非是概念上真的是全域的名稱,否則都不鼓勵使用全域變數,因此應避免 global 宣告的使用。

Python 有個 nonlocal,可以指明變數並非區域變數,請直譯器依照區域函式、外包函式、全域、內建的順序來尋找變數,就算是指定運算時,也要求是這個順序。例如:

x = 10
def outer():
    x = 100         # 這是在 outer 函式範圍的 x
    def inner():
        nonlocal x
        x = 1000    # 改變的是 outer 函式的 x
    inner()
    print(x)        # 顯示 1000

outer()
print(x)            # 顯示 10 

Python 沒有區塊範圍變數,因此在流程控制語法區塊中建立的變數,離開區塊之後也可以使用:

>>> if True:
...     x = 10
...
>>> print(x)
10
>>>

變數範圍的討論,雖然略嫌無趣,然而若沒有搞清楚相關規則,很容易就發生名稱衝突,導致一些不可預期的臭蟲,不可不慎。目前暫時是先針對一個模組檔案中相關的範圍進行探討,之後有機會還會探討其他有關範圍的議題。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter