在使用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,會有更詳細的說明。