2.16. 作用域

當 Python 在函式內部查找一個名稱時,它會搜尋一個特定順序的 作用域。理解這個順序就能解釋為何某些賦值會遮蔽外層名稱、為何另一些賦值會修改它們,以及為何巢狀函式能記住它們被定義之處的值。

巢狀方框顯示了位於模組作用域內的區域作用域, 而模組作用域又位於內建作用域之內,箭頭表示 名稱查找會從最內層的框架向外逐層進行。

名稱查找從區域函式作用域開始,並向外逐層走向模組作用域與內建作用域,直到找到相符者為止。

2.16.1. 區域作用域與模組作用域

在函式內部定義的名稱對該函式而言是 區域的,並會在呼叫結束時消失:

def f():
    x = 10
    print(x)

f()
print(x)              # NameError: x is not defined

.py 檔案最上層定義的名稱是 模組層級的(有時稱為 全域的),並在該檔案的任何地方都可見,包括函式內部:

CAMERA = "OpenMV"

def banner():
    print("running on", CAMERA)

從函式內部讀取模組層級的名稱是自動的。但從函式內部 賦值 給該名稱則不然 -- 這個賦值會建立一個新的區域變數,並在該次呼叫的其餘部分遮蔽掉模組層級的那一個:

counter = 0

def bump():
    counter = counter + 1     # UnboundLocalError

左側的 counter 使 counter 成為 bump 中的區域名稱,因此右側的讀取找不到任何值。

2.16.1.1. global 關鍵字

若要真正從函式內部重新賦值給模組層級的名稱,必須先將其宣告為 global

counter = 0

def bump():
    global counter
    counter += 1

請審慎使用 global。會變更隱藏狀態的函式比起接收引數值並回傳新值的函式更難以推理。對於「我需要共用狀態」這種情況,通常的解法是將一個物件(一個列表、一個字典、一個類別實例)作為引數傳入,並改為變更

2.16.2. Lambda

lambda 可在單一運算式中建立一個小型的匿名函式:

square = lambda x: x * x
square(7)             # 49

它完全等同於::

def square(x):
    return x * x

lambda 的本體必須是單一運算式 -- 不能有陳述式,也不能有多行。它的主要用途是將一個極小的函式作為引數傳遞給某個接受函式的對象:

pairs = [("b", 2), ("a", 3), ("c", 1)]
pairs.sort(key=lambda item: item[1])
# [('c', 1), ('b', 2), ('a', 3)]

當本體超出一個運算式時,請改用真正的 def。以 def 為函式命名也會讓它在追溯(traceback)中擁有一個名稱,而 lambda 則沒有。

2.16.3. 閉包

定義在另一個函式內部的函式可以讀取外圍函式作用域中的名稱。內層函式會 捕捉 那些名稱,並在外層呼叫已回傳之後仍持續運作:

def make_adder(n):
    def add(x):
        return x + n
    return add

add5 = make_adder(5)
add10 = make_adder(10)
print(add5(100), add10(100))

輸出::

105 110

add5add10 是兩個各自獨立的函式,各自記住自己的 n。這樣建立並回傳一個客製化內層函式的函式稱為 閉包。這正是一門語言最初之所以需要巢狀函式的主因 -- 它提供了一種將某些狀態烘焙進函式值中、再將該值作為單一可呼叫物件交付出去的方法。

讀取被捕捉的名稱是自動的。但 重新繫結 其中一個則需要一個額外的關鍵字。下面的範例做了看似正確的事卻失敗了:

def make_counter():
    count = 0
    def tick():
        count = count + 1     # UnboundLocalError
        return count
    return tick

tick 內部對 count 的賦值使 count 成為 tick 的區域變數,正如它在最上層函式中也會使其成為區域變數一樣。nonlocal 關鍵字會告訴 Python「這個名稱住在外圍函式中,請在那裡重新繫結它」:

def make_counter():
    count = 0
    def tick():
        nonlocal count
        count += 1
        return count
    return tick

c = make_counter()
print(c(), c(), c())

輸出::

1 2 3

nonlocal 之於外圍函式作用域,正如 global 之於模組作用域。請注意,變更 一個被捕捉的物件(呼叫 some_list.append(...)some_dict[k] = v)並不需要 nonlocal -- 因為名稱並未被重新繫結,被改變的只是它所指向的物件。