iT邦幫忙

2023 iThome 鐵人賽

DAY 24
0
Software Development

Python十翼:與未來的自己對話系列 第 24

[Day24] 八翼 - Scopes:常見錯誤2(global與nonlocal)

  • 分享至 

  • xImage
  •  

在使用globalnonlocal時,有一個常見的錯誤,我們以兩個例子來說明。

global

問題觀察

# 01a中:

  • 生成一個a變數於G,其值為0
  • 定義一個function my_func,於其中使用global a後,並將a加上1
  • 使用my_func.a,設定my_fucaa
  • 連續呼叫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 = amy_func.a指到a,這個a就是一開始a=0a,而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被執行時,可以取得已經是globala。只是這麼一來,我們不能使用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,其可以有一個attributefunction告知我們,被裝飾的function被呼叫了幾次。

問題觀察

# 02a中:

  • 定義一個decorator function my_counter
    • my_counter中定義count=0
    • wrapper中對count使用nonlocal的關鍵字,並將counts加上1
    • 回傳fn搭配*args**kwargs呼叫結果。
    • 使用wrapper.counts = counts,設定wrappercountscounts
    • 最後回傳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 functiondecorator class兩種方法來試著解決問題。

解決方法1

# 02b我們使用了和上面小題類似的解法,用一個get_counts function來取得nonlocalcounts

# 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

解決方法2

# 02c中:

  • 定義一個decorator classMyCounter
  • __init__接收被裝飾的function,存為self._fn。此外也順便定義了self._count=0,作為底層真正記錄呼叫次數的變數。
  • __call__中我們將self._count加上1,並回傳self._fn搭配__call__所接收argskwargs的呼叫結果。
  • 定義一個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,會有更詳細的說明。

Code

本日程式碼傳送門


上一篇
[Day23] 八翼 - Scopes:常見錯誤1(LEGB原則)
下一篇
[Day25] 九翼 - Exception Groups與except*:導讀PEP654
系列文
Python十翼:與未來的自己對話30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言