見出し画像

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関数オブジェクト

# 出力結果
this is my_decorator
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())
画像1

#実行結果
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())

#実行結果
('fz5050',)
{}
HELLO FZ5050 !
------
()
{'name': 'fz5050'}
HELLO FZ5050 !
------
()
{}
HELLO !

■補足:可変長引数について
・「*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))
   

# 実行結果
0
1
1
2
3
5
8
13
21
34

■実行速度を確認
キャッシュの有無による速度の違いを確認する、ここでは「Timerオブジェクト」をつかって、デフォルト値:1000000回繰り返し実行する。

from timeit import Timer

t = Timer('fibonacci(10)','from __main__ import fibonacci')
print(f'time : {t.timeit()}') # 1000000回コールして平均速度を計測

#実行結果
time : 16.473931800000003

※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("----")

#実行結果
{0: 0, 1: 1}
this is 0
0
----
{0: 0, 1: 1}
this is 1
1
----
{0: 0, 1: 1}
{0: 0, 1: 1}
this is 1
{0: 0, 1: 1}
this is 0
1
----
{0: 0, 1: 1, 2: 1}
{0: 0, 1: 1, 2: 1}
this is 2
{0: 0, 1: 1, 2: 1}
this is 1
2
----
{0: 0, 1: 1, 2: 1, 3: 2}
{0: 0, 1: 1, 2: 1, 3: 2}
this is 3
{0: 0, 1: 1, 2: 1, 3: 2}
this is 2
3
----

■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回コールして平均速度を計測

#実行結果
time : 0.0902396
(※キャッシュなし時のtime : 16.473931800000003)

▲キャッシュがあるので処理時間が短くなる。

【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__) # ドキュメンテーション文字列出力

#実行結果
0
1
1
2
3
5
8
13
21
34
<function memorize.<locals>.wrapper at 0x000001B36E6090D0>
wrapper
this is wrapper

▲改善点として、プログラム上は「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__) # ドキュメンテーション文字列出力

#実行結果
0
1
1
2
3
5
8
13
21
34
<function fibonacci at 0x0000017287AED550>
fibonacci
this is fibonacchi

▲返ってきている関数名は「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回コールして平均速度を計測(キャッシュあり)

#実行結果例
(0,):cached
answer -----> 0
(1,):cached
answer -----> 1
(2,):cached
answer -----> 1
(3,):cached
answer -----> 2
(4,):cached
answer -----> 3
(5,):cached
answer -----> 5
(6,):cached
answer -----> 8
(7,):cached
answer -----> 13
(8,):cached
answer -----> 21
(9,):cached
answer -----> 34
(10,):cached
answer -----> 55
(11,):cached
answer -----> 89
(12,):cached
answer -----> 144
(13,):cached
answer -----> 233
(14,):cached
answer -----> 377
(15,):cached
answer -----> 610
(16,):cached
answer -----> 987
(17,):cached
answer -----> 1597
(18,):cached
answer -----> 2584
(19,):cached
answer -----> 4181
(20,):cached
answer -----> 6765
(21,):cached
answer -----> 10946
(22,):cached
answer -----> 17711
(23,):cached
answer -----> 28657
(24,):cached
answer -----> 46368
(25,):cached
answer -----> 75025
(26,):cached
answer -----> 121393
(27,):cached
answer -----> 196418
(28,):cached
answer -----> 317811
(29,):cached
answer -----> 514229
<function fibonacci at 0x00000221ED4C10D0>
fibonacci
this is fibonacchi
---- start Timer test------
(30,):cached
time : 0.1631084

▲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あり)

#実行結果例
answer -----> 0
answer -----> 1
answer -----> 1
answer -----> 2
answer -----> 3
answer -----> 5
answer -----> 8
answer -----> 13
answer -----> 21
answer -----> 34
answer -----> 55
answer -----> 89
answer -----> 144
answer -----> 233
answer -----> 377
answer -----> 610
answer -----> 987
answer -----> 1597
answer -----> 2584
answer -----> 4181
answer -----> 6765
answer -----> 10946
answer -----> 17711
answer -----> 28657
answer -----> 46368
answer -----> 75025
answer -----> 121393
answer -----> 196418
answer -----> 317811
answer -----> 514229
---- start Timer test------
time : 0.056402999999999995

もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。