依照維基百科的定義,特性導向程式設計(Aspect-oriented programming,AOP)是一種設計模式(Design pattern),它可以自核心程式抽離特定的關注點(Separation of concerns),使程式架構變得更簡潔與模組化,特定的關注點可以是下列功能:
圖一. 自核心程式抽離各種關注點
而Python的裝飾器(Decorator)就是實踐AOP的絕佳工具,它透過宣告的方式,去修飾另一個函數,將被修飾的函數原先要處理的關注點交給Decorator統一處理,讓程式設計師聚焦在商業邏輯的開發,不須要求每一個函數或事件處理都要去檢查使用權限或工作日誌寫入。
在開始撰寫Decorator之前,先看看Decorator的使用方式,以提供Flask、Streamlit套件為例。
範例1. 在Flask程式中,利用Decorator(@app.route)指定getname函數的URL為【/getname】,而且只能接收get方式的請求,完全不需額外撰寫程式碼,只須註記或宣告。
from flask import Flask, request
app = Flask(__name__)
@app.route("/getname", methods=['GET']) # Decorator
def say_hello(): # 事件處理函數
name = request.args.get('name')
return "Hello "+name
if __name__ == '__main__':
app.run()
如果不使用【@app.route("/getname", methods=['GET'])】,我們就必須在每一個函數中撰寫判斷式,確認是get或post,並且另外撰寫ㄧ主程式,進行路由的管理。
檔案儲存為flask_test.py。
測試:須先安裝Flask,再執行【python flask_test.py】啟動server,在瀏覽器輸入http://localhost:5000/getname?name=michael 。
執行結果:Hello michael。
也可以使用Postman測試,改用post,會得到以下錯誤:
圖二. 使用post送出請求
執行結果:得到【405 Method Not Allowed】,表/getname不允許post。
範例2. 在Streamlit程式中,指定load_data函數輸出可存入快取(Cache),只要呼叫的輸入參數(url)相同,就會改由Cache取得輸出結果,而不是每次都重新讀取檔案。
@st.cache_data # caching decorator
def load_data(url):
df = pd.read_csv(url)
return df # df 會自動存入Cache
df = load_data("https://github.com/plotly/datasets/raw/master/uber-rides-data1.csv")
st.dataframe(df)
如果不使用【@st.cache_data】,我們就必須在每一個函數中撰寫判斷式,確認是要使用Cache還是重新讀取檔案。
在GitHub檔案名稱為streamlit_cache.py。
測試:須先安裝Streamlit,必須使用下列指令啟動server,Streamlit會自動帶出瀏覽器。
streamlit run streamlit_cache.py
以上範例說明,使用AOP/Decorator,自核心程式抽離關注點,可以讓我們少寫很多程式,而且程式碼會變得非常簡潔。
Decorator可以是一個函數(Function)或類別(Class),用以擴展其他函數的功能,但不須更改其他函數的程式碼,只要在函數前面加上註解(Annotation)即可。
範例3. Decorator 簡單實作,檔案名稱為decorator1.py。
# 宣告Decorator函數
def log_decorator(func):
# inner function
def wrapper(): # inner function
print("呼叫函數前")
func() # 執行say_hello函數
print("呼叫函數後")
# 呼叫 inner function
return wrapper
# 為 say_hello 函數加上 Decorator
@log_decorator
def say_hello():
print("hello !")
# 呼叫 say_hello
say_hello()
python decorator1.py
呼叫函數前
hello !
呼叫函數後
範例4. 顯示執行的流程及執行時間,檔案名稱為decorator2.py。
import time
# 宣告Decorator函數
def log_decorator(func):
# inner function
def wrapper():
# print("呼叫函數前")
start_time = time.time()
func()
print(f"【{func.__name__}】 執行時間:{(time.time() - start_time)} 秒")
# print("呼叫函數後")
# 呼叫 inner function
return wrapper
@log_decorator
def func_a():
print("hello a!")
@log_decorator
def func_b():
total = 0
for i in range(100_001):
total += i
print("hello b!")
@log_decorator
def func_c():
print("hello c!")
if __name__ == "__main__":
func_a()
func_b()
func_c()
hello a!
【func_a】 執行時間:0.0009965896606445312 秒
hello b!
【func_b】 執行時間:0.006021738052368164 秒
hello c!
【func_c】 執行時間:0.0010008811950683594 秒
範例5. 顯示函數的參數名稱及其內容,檔案名稱為decorator3.py。
import time, inspect
# 宣告Decorator函數
def log_decorator(func):
# inner function
def wrapper(*arg1, **arg2):
# print("呼叫函數前")
start_time = time.time()
func(*arg1, **arg2)
print(f"【{func.__name__}】執行時間:{(time.time() - start_time)} 秒")
print(f"\t參數:", end=' ') # {inspect.signature(func)}: {arg1}")
for name, value in zip(inspect.signature(func).parameters.keys(), arg1):
print(f"{name}:{value}", end='\t')
print(f"\t\t")
# print("呼叫函數後")
# 呼叫 inner function
return wrapper
@log_decorator
def func_a(x):
print("hello a!")
@log_decorator
def func_b(x1, x2):
total = 0
for i in range(100_001):
total += i
print("hello b!")
@log_decorator
def func_c(y1, y2, y3):
print("hello c!")
if __name__ == "__main__":
func_a(1)
func_b(2, 3)
func_c(4, 5, 6)
hello a!
【func_a】執行時間:0.0010006427764892578 秒
參數: x:1
hello b!
【func_b】執行時間:0.00599980354309082 秒
參數: x1:2 x2:3
hello c!
【func_c】執行時間:0.0009999275207519531 秒
參數: y1:4 y2:5 y3:6
本篇說明AOP概念、套件如何使用Decorator以及如何撰寫Decorator應用在工作日誌(Log)上,下一篇會繼續介紹Decorator更多的功能,Happy coding!!
本系列的程式碼會統一放在GitHub,本篇的程式放在src/6資料夾,歡迎讀者下載測試,如有錯誤或疏漏,請不吝指正。