發表於 Python問題解答教學

【Python】Function 也有靜態變數?! Feat. Decorator

先說結論,這篇文章會講解到

  • Function 靜態變數設置
  • 利用 Python Decorator幫助設置 Function Variable
  • 和更進階的製作 Decorator 設定方法

前言:

剛剛吃完年夜飯,突然心血來潮想回答一下PTT Python版上的問題
所以就上來寫一篇我知道的解法和我後來又看到新的解法啦!
我遇到的問題是這個: PTT Python 版問題

原PO希望 function 內有一個變數(預設 global)可以記錄呼叫的次數。

我個人在寫 Python 時其實蠻討厭 global 變數的

因為我非常討厭寫到一半發現錯誤的原因竟然是變數覆蓋之類的問題(會覺得忙了很久卻只因為一個小錯誤!!!!

所以來分享一下我自己會用的做法吧!(順便回復原PO問題XD

Function 靜態變數設置

不知道大家有沒有發現一件事情

Python 的世界都是 Object

沒錯! 正因為如此,我們可以利用這特性來新增 Function Static Variable!
( Python 的世界都是 Object 這件事我想都可以再寫一篇解釋了…XD

畢竟現在是過年,我就用水餃店紀錄帳本的例子舉例好了!
先上 Basic Code:

def orderDumplings(num: int):
    print(f"收到一個訂單 水餃 {num} 個")

for i in range(1, 6):
    orderDumplings(i)

orderDumplings 只是一個收到水餃訂單然後 print 出收到訂單資訊的簡單 function。然後用 for 迴圈傳假資料進去,

但是現在我想要記錄每天收到幾次訂單

就是紀錄 function 被呼叫多少次

我可以直接設置 orderDumplings.orders += 1
像是~

def orderDumplings(num: int):
    orderDumplings.orders += 1  # 直接 將被呼叫的次數 += 1 即可
    print(f"收到一個訂單 水餃 {num} 個")

orderDumplings.orders = 0 # 在 function定義完之後 初始化靜態變數

for i in range(1,6):
    orderDumplings(i)

print(f"今天接到 {orderDumplings.orders} 位客人的訂單") # 不管是function 內部或外部都可以呼叫

這樣的話 就不用再利用 global 變數也不會再被變數生命區塊搞到了!
不過要記得,這樣做的變數名稱不可以用到原本 function 內建變數的名稱
不然的話會覆蓋到而導致錯誤的!!!

利用 Python Decotator 幫助設置 Function Variable

向上個例子那樣設計的話絕對是沒問題的,但是我們每次設定好一個新的 function 靜態變數 就要在外面多寫一行初始化它,這樣豈不是非常麻煩嗎?
懶惰如我
接下來我們就來用 decorator 來簡化設定過程吧!
如果還不懂 Python Decorator 的話可以看這篇了解喔!

如果想要用 Decorator 來設置靜態變數的話,可以向下面這樣做

def setVar(varname: str, val: object):
    def wrapper(originFunction):
        def inner(*arg, **kwarg):
            return originFunction(*arg, **kwarg)
        setattr(inner, varname, val)
        return inner  # wrapper return
    return wrapper

@setVar("orders", 0)
def orderDumplings(num: int):
    orderDumplings.orders += 1
    print(f"收到一個訂單 水餃 {num} 個")

for i in range(1, 6):
    orderDumplings(i)

print(f"今天接到 {orderDumplings.orders} 位客人的訂單")

說明一下程式的運作吧!

for i in range(1, 6):
    orderDumplings(i)

print(f"今天接到 {orderDumplings.orders} 位客人的訂單")

這段只是為了測試功能正確不正確而寫的, for 迴圈會讓 orderDumpling 下訂單定水餃的 function 執行五次,每次執行都會讓 orderDumplings的靜態變數 orders +1 而 orderDumplings.orders 的初始化是在上面的@setVar(“orders", 0中設定的。

為了之後設定更方便,在 function 宣告前加上 @setVar(“variable Name", initVal修飾器。

並且設定有參數的修飾器(第1~7行),想複習為什麼要用3個function設定的話可以看這篇~(突然發現好險以前有做這系列的教學分享,不然這邊會變超長XD)

至於為什麼變數名稱可以使用字串str的方式設定呢?
我簡短說明一下,是因為在 Python 物件中變數就是屬性的一種,而屬性會被存在 物件的dict中,key就是它的變數名稱。

如果想要試試看的話,可以複製上面那段程式碼自己跑跑看,或是改改看會更理解喔!

更進階的製作 decorator 設定方法

不過上一段程式碼有一點小地方可以改進!
聰明的你們一定有發現,像是像剛剛那樣自行設定 decorator 的話會有 function.__name__被更改到的困擾,也因為如此,我們只可以使用剛剛的@setVar 一次而已,但是這樣絕不是我們希望的,有時我們會需要設定多個變數。

所以我們可以利用functools.wraps來幫助我們設定function的一些內部複雜設定吧。

先上程式碼讓大家跑跑看吧!

from functools import wraps

def setVar(varname: str, val: object):
    def wrapper(originFunction):
        @wraps(originFunction)
        def inner(*arg, **kwarg):
            return originFunction(*arg, **kwarg)
        setattr(inner, varname, val)
        return inner  # wrapper return
    return wrapper

@setVar("orders", 0)
@setVar("money", 3000)
def orderDumplings(num: int):
    orderDumplings.orders += 1
    orderDumplings.money += 100
    print(f"收到一個訂單 水餃 {num} 個")

for i in range(1, 6):
    orderDumplings(i)

print(f"今天接到 {orderDumplings.orders} 位客人的訂單")
print(f"總存款: {orderDumplings.money}")

這邊我們新增了一個變數 money 紀錄每次收到訂單我們的存款就會有所更動(當然是增加啦!)

其他做法都跟剛剛的做法一樣,唯一不同的是 在設定inner function之前我們要先把 originFunction放入 wraps 這個修飾器讓他幫忙我們設定內部的複雜設定值。這樣一來我們就可以恢復一開始的設定,例如:function.__name__ 這種設定,也就可以讓我們的 decorator 使用多次了!

結尾:

這篇又是一篇從去年寫到今年的文章呢!XD
上一篇是發生在國曆年,這一篇是發生在農曆年XDDDD

說起來我很喜歡逛 python 版看看大家都遇到什麼問題,多看看這樣以後我遇到同樣問題就可以馬上想到怎麼解決了~
這次想說既然我會一種解法就上來解答了
不過上網查了一下發現 decorator 這種做法真的比較方便(應該吧?)
我也就因此學了一課呢!

就這樣啦!如果有什麼問題或是有更好的做法的話都可以在下方討論喔!
或是有想說的什麼話都可以留言跟我說喔!
那就下一篇再見啦!
ㄅㄅ

作者:

一位 熱愛資工領域、喜歡好笑事物、偶爾打打網球 的學生 ! For A Better Me!

發表迴響

Please log in using one of these methods to post your comment:

WordPress.com 標誌

您的留言將使用 WordPress.com 帳號。 登出 /  變更 )

Google photo

您的留言將使用 Google 帳號。 登出 /  變更 )

Twitter picture

您的留言將使用 Twitter 帳號。 登出 /  變更 )

Facebook照片

您的留言將使用 Facebook 帳號。 登出 /  變更 )

連結到 %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.