DP.08:既存の関数をラッピングして処理を追加する。- Decorator -【Python】
【1】Decorator概要
既存の関数内に処理を書き足すことなく、前後に処理を追加する、拡張する、などができるようにする書き方。例えば次のような使い方がある。
【2】予備知識:@デコレータについて
簡単にデコレータの使い方についてまとめておく。
■2-1:@をつけた書き方
(例1):関数を引数にうけとる関数:my_decorator
# 引数に関数オブジェクトをうけとるデコレータ
def my_decorator(func):
print('this is my_decorator')
return func
# wrapping(デコレート)される関数
def say_hello():
return 'Hello!'
## 動作確認 ###
greeting = my_decorator(say_hello) # 引数に関数オブジェクトをわたす
print(greeting()) # greetingオブジェクトはsay_hello関数オブジェクト
プログラムに記述している通り、引数に関数オブジェクトを渡せばよいのだが、これに対する「syntax sugar(糖衣構文)」が「@をつけた書き方」である。
(例2):@デコレータで書いた場合
# 引数に関数オブジェクトをうけとるデコレータ
def my_decorator(func):
print('this is my_decorator')
return func
# wrapping(デコレート)される関数
@my_decorator
def say_hello():
return 'Hello!'
## 動作確認 ###
#greeting = my_decorator(say_hello)
#print(greeting())
print(say_hello())
■2-2:関数の中に関数を記述してwrapping(デコレート)する
関数の中にさらに関数を定義して呼び出すことで、もともとの関数の返却値を加工していくのが定番の書き方。
(例3):関数の中に関数を記述して加工する
# 引数に関数オブジェクトをうけとるデコレータ
def my_decorator(func):
# 関数の中に関数を定義
def wrapper():
tmp_result = func() # 引数に渡された関数(オブジェクト)をコール
result = tmp_result.upper() # upper処理
return result # 処理後の結果を返す
# すぐ上で定義した関数をコール
return wrapper
# wrapping(デコレート)される関数
@my_decorator
def say_hello():
return 'Hello!'
## 動作確認 ###
print(say_hello())
■2-3:引数を持つ関数にデコレータをつかう
引数を持つ関数にデコレータを使うには可変長引数「*args」「**kwargs」を使う。
(例4):引数を持つ関数にデコレータを使う
# 引数に関数オブジェクトをうけとるデコレータ
def my_decorator2(func):
# 関数の中に関数を定義
def wrapper(*args, **kwargs):
print(args)
print(kwargs)
tmp_result = func(*args, **kwargs)
result = tmp_result.upper()
return result
# すぐ上で定義した関数をコール
return wrapper
# wrapping(デコレート)される関数
@my_decorator2
def say_byebye(name=""):
return f'Hello {name} !'
## 動作確認 ###
print(say_byebye("fz5050"))
print("------")
print(say_byebye(name="fz5050"))
print("------")
print(say_byebye())
■補足:可変長引数について
・「*args」は追加で指定した「引数値」を受けつける
・「**kwargs」は追加で指定した「キーワードと引数値」を受けつける
※指定せずに空でもOK。
(例5):可変長引数の指定の仕方による動作の違い
# 引数numberは指定必須、他は空でもOK
def my_func(number, *args, **kwargs):
print(number)
print(args)
print(kwargs)
my_func(1) # number=1に値をセットされる
print("-------")
my_func(1, 2, 3) # number = 1, args=(2,3)に値をセットされる
print("-------")
my_func(1, 2, 3, x=99) # number = 1, args=(2,3), kwargs={'x': 99}に値をセットされる
print("-------")
my_func(1, x=99) # number = 1, kwargs={'x': 99}に値をセットされる
print("-------")
my_func(number=99) # number = 99に値をセットされる
print("-------")
my_func(x=99) # エラー。numberへの値設定がない
print("-------")
なお、「args」と「kwargs」という名前は慣例的なものなので、実はなんでもいい(例「*param」とか「**argv」みたいな名前など)。しかしながら、pythonプログラム中でよくみる書き方のほうが他の人が見てもわかりやすくなる可能性がある。
【3】例:再帰処理(フィボナッチ数列)にキャッシュをもたせる
例として、「再帰処理で記述したフィボナッチ数列を求める関数」に「Decoratorパターン」で「キャッシュ」を仕込むことを考えてみる。
■第n項に対するフィボナッチ数列を返す関数(キャッシュなし)
#再帰処理で階層がn次第で大きくなる(→キャッシュをもたせたい)
def fibonacci(n):
if n in (0,1):
return n
res = fibonacci(n-1) + fibonacci(n-2)
return res
for num in range(10):
print(fibonacci(num))
■実行速度を確認
キャッシュの有無による速度の違いを確認する、ここでは「Timerオブジェクト」をつかって、デフォルト値:1000000回繰り返し実行する。
from timeit import Timer
t = Timer('fibonacci(10)','from __main__ import fibonacci')
print(f'time : {t.timeit()}') # 1000000回コールして平均速度を計測
※Timerオブジェクトについては以下参照
■Timer.timeit
■使用例
↓ これにキャッシュを持たせる
■第n項に対するフィボナッチ数列を返す関数(専用キャッシュあり、デコレータ未適用)
my_cache = {0:0, 1:1} # 専用キャッシュを持たせる
def fibonacci(n):
print(my_cache)
if n in my_cache:
print(f'this is {n}')
return my_cache[n]
res = fibonacci(n-1) + fibonacci(n-2)
my_cache[n] = res
return res
for num in range(5): # n = 0, 1, 2, 3, 4を求める
print(fibonacci(num))
print("----")
■Timerを使って時間計測、キャッシュの効果を確認する
from timeit import Timer
my_cache = {0:0, 1:1} # 専用キャッシュ
def fibonacci(n):
#print(my_cache)
if n in my_cache: # キャッシュ内にnがあるか
#print(f'this is {n}')
return my_cache[n]
res = fibonacci(n-1) + fibonacci(n-2)
my_cache[n] = res
return res
t = Timer('fibonacci(10)','from __main__ import fibonacci')
print(f'time : {t.timeit()}') # 1000000回コールして平均速度を計測
▲キャッシュがあるので処理時間が短くなる。
【4】デコレータを実装する
次にデコレータも組み込む。
■第n項に対するフィボナッチ数列を返す関数(キャッシュ+デコレータあり、改善点もアリ)
def mymemorize(func):
cache = dict() # キャッシュ格納用
def wrapper(*args, **kwargs):
"""this is wrapper""" # __doc__ 出力用
if args not in cache: # キャッシュになければキャッシュにも追加
cache[args] = func(*args, **kwargs)
return cache[args] # キャッシュデータから返す
return wrapper
@mymemorize
def fibonacci(n):
"""this is fibonacchi""" # __doc__ 出力用
if n in (0, 1):
return n
res = fibonacci(n-1) + fibonacci(n-2)
return res
for n in range(10):
print(fibonacci(n)) # n=0~9までキャッシュが作られる
print(fibonacci)
print(fibonacci.__name__) # 関数の名前を出力
print(fibonacci.__doc__) # ドキュメンテーション文字列出力
▲改善点として、プログラム上は「fibonacciという名前の関数」をコールしているが、返ってきている関数名は「wrapper」である点。
計算結果としては問題ない。しかし、例えばデバッグするとき等、デコレート(ラッピング)されている本来の関数をトレースできない可能性もある。
これは「functools.wraps」を使って解決する。
■第n項に対するフィボナッチ数列を返す関数(キャッシュ+デコレータあり、完全版)
import functools
def memorize(func):
cache = dict() # キャッシュ格納用
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""this is wrapper""" # __doc__ 出力用
if args not in cache: # キャッシュになければキャッシュにも追加
cache[args] = func(*args, **kwargs)
return cache[args] # キャッシュデータから返す
return wrapper
@memorize
def fibonacci(n):
"""this is fibonacchi""" # __doc__ 出力用
if n in (0, 1):
return n
res = fibonacci(n-1) + fibonacci(n-2)
return res
for n in range(10):
print(fibonacci(n)) # n=0~9までキャッシュが作られる
print(fibonacci)
print(fibonacci.__name__) # 関数の名前を出力
print(fibonacci.__doc__) # ドキュメンテーション文字列出力
▲返ってきている関数名は「fibonacci」になった。
【5】全体コード
Timer.timeit()を使った実行時間の計測も含めた全体コードは次のような感じ。※n=30で計算を実施。
import functools
from timeit import Timer
def mymemorize(func):
cache = dict() # キャッシュ格納用
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""this is wrapper""" # __doc__ 出力用
if args not in cache: # キャッシュになければキャッシュにも追加
cache[args] = func(*args, **kwargs)
print(f"{args}:cached")
return cache[args] # キャッシュデータから返す
return wrapper
@mymemorize
def fibonacci(n):
"""this is fibonacchi""" # __doc__ 出力用
if n in (0, 1):
return n
res = fibonacci(n-1) + fibonacci(n-2)
return res
for n in range(30):
print(f'answer -----> {fibonacci(n)}') # ここで0~29までキャッシュがつまれる
#print(fibonacci(10))
print(fibonacci)
print(fibonacci.__name__)
print(fibonacci.__doc__)
print("---- start Timer test------")
# n=30のキャッシュが1度起動。あとはキャッシュからすべて結果を返す
t = Timer('fibonacci(30)','from __main__ import fibonacci')
print(f'time : {t.timeit()}') # 1000000回コールして平均速度を計測(キャッシュあり)
▲Timer計測での1000000回コールでは、n=30を1度キャッシュした後はキャッシュから値を返すため高速化されている。
【6】おまけ:functools.cacheを使う
今回はデコレータの練習として再帰処理のキャッシュを例として挙げ、独自にmemorize(キャッシュ)機能を実装した。しかし、memorize(キャッシュ)機能については、functoolsがその機能を提供している。
ドキュメントにはいくつかあるが、例えば「functools.cache」を使うと次のような感じで記述できる。
■functools.cacheでデコレータ+キャッシュをする
import functools
from timeit import Timer
@functools.cache
def fibonacci(n):
if n in (0, 1):
return n
res = fibonacci(n-1) + fibonacci(n-2)
return res
for n in range(30):
print(f'answer -----> {fibonacci(n)}')
print("---- start Timer test------")
t = Timer('fibonacci(30)','from __main__ import fibonacci')
print(f'time : {t.timeit()}') # 1000000回コールして平均速度を計測(functools.cacheあり)