在使用global與nonlocal時,有一個常見的錯誤,我們以兩個例子來說明。
global# 01a中:
a變數於G,其值為0。function my_func,於其中使用global a後,並將a加上1。my_func.a,設定my_fuc的a為a。my_func兩次。此時,觀察my_func.a,會發現其值為0,而不是預期的2,您能看出為什麼嗎?
# 01a
a = 0
def my_func():
global a
a += 1
my_func.a = a
my_func(), my_func()
print(f'{my_func.a=}') # 0
print(f'{a=}') # 2
這是因為my_func.a = a將my_func.a指到a,這個a就是一開始a=0的a,而a又指到0。後來呼叫my_func,只會讓a這個符號指到新計算出來的值,是一個新的記憶體位置。
我們可以藉由印出各階段a的記憶體位置來確認。
# 01b
a = 0
print(f'Begin: {id(a)=}')
print(f'{id(0)=}')
def my_func():
global a
print(f'Begin(my_func): {id(a)=}')
a += 1
print(f'End(my_func): {id(a)=}')
my_func.a = a
my_func(), my_func()
print(f'{my_func.a=}') # 0
print(f'{a=}') # 2
print(f'{id(my_func.a)=}')
print(f'End: {id(a)=}')
Begin: id(a)=140726095700744
id(0)=140726095700744
Begin(my_func): id(a)=140726095700744
End(my_func): id(a)=140726095700776
Begin(my_func): id(a)=140726095700776
End(my_func): id(a)=140726095700808
my_func.a=0
a=2
id(my_func.a)=140726095700744
End: id(a)=140726095700808
我們可以看出my_func.a與最終a的記憶體位置不一致。
我們需要一個get_a function作為my_func.a。當get_a被執行時,可以取得已經是global的a。只是這麼一來,我們不能使用my_func.a,而必須使用my_func.a()才能取得a。
# 01c
a = 0
print(f'Begin: {id(a)=}')
print(f'{id(0)=}')
def my_func():
global a
print(f'Begin(my_func): {id(a)=}')
a += 1
print(f'End(my_func): {id(a)=}')
def get_a():
return a
my_func.a = get_a
my_func(), my_func()
print(f'{my_func.a()=}') # 2
print(f'{a=}') # 2
print(f'{id(my_func.a())=}')
print(f'End: {id(a)=}')
Begin: id(a)=140726095700744
id(0)=140726095700744
Begin(my_func): id(a)=140726095700744
End(my_func): id(a)=140726095700776
Begin(my_func): id(a)=140726095700776
End(my_func): id(a)=140726095700808
my_func.a()=2
a=2
id(my_func.a())=140726095700808
End: id(a)=140726095700808
我們可以看出my_func.a()與最終a的記憶體位置是一致的。
nonlocal這個小節我們準備實作一個decorator,其可以有一個attribute或function告知我們,被裝飾的function被呼叫了幾次。
# 02a中:
decorator function my_counter。
my_counter中定義count=0。wrapper中對count使用nonlocal的關鍵字,並將counts加上1。fn搭配*args與**kwargs呼叫結果。wrapper.counts = counts,設定wrapper的counts 為counts。wrapper。my_counter裝飾在my_func上。my_func兩次。此時,觀察my_func.counts,會發現其值為0,而不是預期的2,相信這次聰明的您,應該知道問題在哪了吧?
其實原因跟上面小題是類似的,wrapper.counts是指著一開始的counts,而counts指的是0。
# 02a
from functools import wraps
def my_counter(fn):
counts = 0
@wraps(fn)
def wrapper(*args, **kwargs):
nonlocal counts
counts += 1
return fn(*args, **kwargs)
wrapper.counts = counts
return wrapper
@my_counter
def my_func():
pass
my_func(), my_func()
print(my_func.counts) # 0
接下來,我們分別使用decorator function及decorator class兩種方法來試著解決問題。
# 02b我們使用了和上面小題類似的解法,用一個get_counts function來取得nonlocal的counts。
# 02b
from functools import wraps
def my_counter(fn):
counts = 0
@wraps(fn)
def wrapper(*args, **kwargs):
nonlocal counts
counts += 1
return fn(*args, **kwargs)
def get_counts():
return counts
wrapper.counts = get_counts
return wrapper
@my_counter
def my_func():
pass
my_func(), my_func()
print(my_func.counts()) # 2
方法1在呼叫兩次my_func()後,可以順利取得2。請注意,如果採用這個方法,我們必須使用my_func.counts()來取得counts。
# 02c中:
decorator class為MyCounter。__init__接收被裝飾的function,存為self._fn。此外也順便定義了self._count=0,作為底層真正記錄呼叫次數的變數。__call__中我們將self._count加上1,並回傳self._fn搭配__call__所接收args及kwargs的呼叫結果。counts property幫助我們回傳底層的self._counts。MyCounter裝飾在my_func上。my_func兩次。# 02c
class MyCounter:
def __init__(self, fn):
self._fn = fn
self._counts = 0
def __call__(self, *args, **kwargs):
self._counts += 1
return self._fn(*args, **kwargs)
@property
def counts(self):
return self._counts
@MyCounter
def my_func():
pass
my_func(), my_func()
print(my_func.counts) # 2
解決方法2在呼叫兩次my_func()後,也可以順利取得2。請注意,如果採用這個方法,可以使用my_func.counts來取得counts(謝謝property)。
decorator function是一般人比較熟悉的,大部份情況,我們也會優先使用decorator function,因為實作起來比較直觀。但是如果當decorator function中使用很多的nonlocal時(有許多狀態需要儲存),或許改用decorator class會是比較好的方法。在這種情況下,解決方法2比解決方法1簡潔不少,而且解決方法2還可以使用my_func.counts取值,不必像解決方法1一樣得使用my_func.counts()。
global時與nonlocal關鍵字時,必須細心確認。decorator內有很多狀態需要儲存時,實作decorator class或許會比decorator function來得方便。本日內容大多收集整理自Python 3:Deep Dive。其中nonlocal的範例可以參考Part 4-Section 04-Polymorphism and Special Methods- 06 Callables,會有更詳細的說明。