Pythonのデコレータ

記事の内容

 この記事では、Pythonのデコレータ(decorator)とは何か、デコレータの作り方と使い方について順に説明していきます。

***わからない用語があるときは索引ページへ***

1.デコレータとは

 デコレータは、作成した関数を変更することなく、関数を修飾できるPythonの仕組です。ここで、「関数を修飾する」というのは、関数実行の前後に機能追加(プログラムを追加)することをいいます。デコレータを使って関数を修飾することを、「関数をデコレーションする」といいます。
 修飾がその場限りのものであれば、関数呼び出しの前後にプログラム文を追加すればよいのですが、実用的なプログラムを作成していると、関数は違えど、共通の機能で修飾したくなることがあります

 例えば、数値計算をする関数の処理時間を計測したい場合を考えてみましょう。処理時間を計測したい関数は1種類だけでなく、多種多様あるとします。
 処理時間を計測するためには、関数実行前のタイムスタンプと実行後のタイムスタンプを取得し、時間差を計算する必要があります。このため、普通の書き方をすると、関数呼び出しの前後に毎回処理時間を計測するたものプログラム文を追加しなければなりません。
 このようなケースにおいて、処理時間を計測するデコレータを予め用意しておくと、もともとの数値計算の関数に処理時間計測の機能を簡単に修飾することができます。
 以下、デコレータの作成に必要となるプログラミングの方法をひとつずつ説明していきます。
 デコレータの仕組をトレースするのは結構面倒です。デコレータの書き方、使い方のパターンだけを確認したい場合は、この記事の最後の「6.デコレータの作成/利用パターンまとめ」を参照ください。ここにデコレータ作成のひな形(テンプレート)を用意しています。
 私自身も、毎回忘れるので、デコレータを使うときはこのひな形から作成しています。

2.関数の中に関数を作成する

 Pythonでは、関数の中に関数を作成することができます。サンプルプログラムで動きを見てみましょう。

##deco_ex1.py
##関数の中に関数を作成する

def tashizan(x, y):
   def inner_func(a, b):
       result = a + b
       
       return result
   wa = inner_func(x, y)
   print(x, "+", y, "=", wa)


##プログラム開始位置
tashizan(5, 3)

実行結果:

5 + 3 = 8

"deco_ex1.py"では、tashizanの中にinner_funcという関数を作成した例です。関数の中で作成した関数(inner_func)は、その上位の関数tasizanのプログラムブロックでのみ利用できます。つまり、関数のスコープは前の記事で説明した変数のスコープと同じです。
 その他のルールについては、これまで説明してきた関数の使い方、作り方と変わりません。
 デコレータでは関数の中に関数を作成するので、先に説明をしました。

3.関数をオブジェクトとして扱う

 関数の呼び出しと作成の記事の中で、関数の呼び出しは、関数の戻り値データのオブジェクトとして扱うことができると説明しました。このサブタイトルの、「関数のオブジェクト」とは、def文で定義される関数本体のオブジェクトのことを言っています。
 関数の定義本体のオブジェクトのやりとりを、サンプルプログラムで見てみましょう。

##deco_ex2.py

def tashizan(x, y):
   wa = x + y
   return wa

def hikizan(x, y):
   sa = x - y
   
   return sa

##引数funcで関数のオブジェクトを受け取って実行
def shisoku_enzan(func, x, y):
   result = func(x, y)
   
   return result


##プログラム実行開始位置
a = 5
b = 3

wa = shisoku_enzan(tashizan, a, b)
sa = shisoku_enzan(hikizan, a, b)

print("wa =", wa)
print("sa =", sa)

実行結果

wa = 8
sa = 2

動きを説明します。

def shisoku_enzan(func, x, y):
   result = func(x, y)
   
   return result

 shisoku_enzan関数は、1つめの引数に関数オブジェクトを受け取って、内部で実行する関数です。1行目で引数としてわたされた関数オブジェクトfuncを実行しています。呼び出し部分をみてみると、

wa = shisoku_enzan(tashizan, a, b)

となっています。この文は、shisoku_enzan関数の最初の引数に、関数tashizanを渡しているので、関数shisoku_enzanの第一引数には、関数tashizanが渡されています。

result = func(x, y)

のfuncには、関数tashizanが渡されているので、この文では

result = tashizan(x, y)

が実行されていることになるのです。

 Pythonで関数を呼び出して実行するときには、

<関数名>(<引数1>, <引数2>, ・・・)

で実行し、これは関数の戻り値のオブジェクトとして扱われます。
 一方、Pythonで関数名だけでオブジェクトを参照した場合、引数の()を省略した場合は、関数のオブジェクトとして扱われます
 関数をオブジェクトとして扱えるということは、関数そのものを別の関数の引数として渡せるだけでなく、変数に代入することもできます

a = tashizan
wa = a(5, 3)

のような書き方もできます。

 デコレータでは関数のオブジェクトをやりとりするので、先に説明をしました。

4.関数を修飾(デコレーション)する

 上記で説明した、関数の中に関数を作成する方法と、関数のオブジェクトをやりとりする方法を使って、元の関数を修飾してみましょう。
 "deco_ex3.py"では、サイコロをふるシミュレーションをする関数に対して、シミュレーションに要した時間を測定して出力する修飾をしてみました。
 サンプルプログラムを上から読むと理解しづらいので、解説の記事に沿って読んでみてください。

##deco_ex3.py

##タイムスタンプを取得するオブジェクトの取り込み
import datetime

##乱数を発生させるオブジェクトの取り込み
import random


##元の関数を引数funcで受け取って、機能追加をする関数
def time_mesurement_deco(func):

   ##内部関数wrapperでfuncに機能追加
   def wrapper(*args, **kwargs):

       ##引数funcに渡された関数の関数名を取得して出力
       func_name = func.__name__
       print("関数"+func_name+"シミュレーション開始...")
       
       ##実行直前のタイムスタンプを取得
       start_time = datetime.datetime.now()
       

       ##funcを実行
       result = func(*args, **kwargs)
       


       ##実行完了直後のタイムスタンプを取得し、処理時間を計算
       finish_time = datetime.datetime.now()
       processing_time = finish_time - start_time
       
       ##処理時間を出力
       print("関数"+func_name+"シミュレーション完了!!!\n")
       print("シミュレーションにかかった時間:", processing_time)
       print("\n")

       ##funcの実行結果を返す
       return result
   
   return wrapper
       
##元の関数。
##1~6の整数の乱数列を発生させ、
##乱数がsmall以上、big以下の確率を計算して返す
def dice_simu(small, big, n=1000000, seed=0):
   random.seed(seed)
   
   hit_num = 0
   for i in range(n):
       deme = random.randrange(1, 7)
       
       if deme >= small and deme <= big:
           hit_num += 1
   
   hit_rate = hit_num / n
   
   return hit_rate




##プログラム実行開始位置
##シミュレーション初期値の設定
small = 4
big = 6
kaisu = 10000000

##関数dice_simuを関数time_mesurement_decの渡すと
##引数にdice_simuが渡された状態の関数time_mesurement_decoの
##内部関数wrapperのオブジェクトが返される
powup_dice_simu = time_mesurement_deco(dice_simu)

##powup_dice_simを実行すると、
##引数にdice_simuが渡された状態の関数time_mesurement_decoの
##内部関数wrapperが実行される
##関数wrpperの中のfunは関数dice_simu
hit_rate = powup_dice_simu(small, big, n=kaisu, seed=1.5)

print("さいころをふった回数:", kaisu, "回")
print("出目が", small, "以上", big, "以下の確率:", hit_rate)

実行結果は以下のとおりです。サイコロを1000万回ふるシミュレーションに要した時間約10.6秒、4-6の目が出た確率は約50%という結果でした。

関数dice_simuシミュレーション開始...
関数dice_simuシミュレーション完了!!!
シミュレーションにかかった時間: 0:00:10.563391

さいころをふった回数: 10000000 回
出目が 4 以上 6 以下の確率: 0.5000407

以下、プログラム実行開始位置からサンプルプログラムの説明をしていきます。

##シミュレーション初期値の設定
small = 4
big = 6
kaisu = 10000000

シミュレーション関数dice_simuの引数の初期値を設定しています。
関数dice_simuはサイコロを1000万回振って、出目が4以上、6以下の確率を計算します。

powup_dice_simu = time_mesurement_deco(dice_simu)

関数dice_simuを関数time_mesurement_decoの引数として渡しています。関数time_mesurement_decoを見てみましょう。

def time_mesurement_deco(func):

   ##内部関数wrapperでfuncに機能追加
   def wrapper(*args, **kwargs):

       <wrapperの中身は省略> 
   
   return wrapper

関数time_mesurement_decoの引数funcに関数dice_simuが渡されました。
内部関数のwrapperは、内部で定義されているだけで、この関数の中では実行されていませんが、最後の行の

return wrapper

で、関数wrapperを返しています。このとき、外側の関数 time_mesurement_decoの引数funcに、関数dice_simuが渡された状態の関数wrapperが返されます。

---以下、補足---
 正確に表現すると、return文で関数wrapperオブジェクトを参照渡しで返しているのですが、関数wrapperは関数time_mesurement_decoの内部関数です。time_mesurement_decoは、呼び出されたときに引数funcに関数dice_simuがセットされた状態でメモリ上に展開されますが、この内部関数であるwrapperのインスタンス(メモリ展開済のオブジェクト)が参照渡しで返されているのです。
---補足ここまで---

 もう一度関数time_mesurement_decoの呼び出しを見てみましょう。

powup_dice_simu = time_mesurement_deco(dice_simu)

 外側の関数 time_mesurement_decoの引数funcに、関数dice_simuが渡された状態の関数wrapperが返され、それがpowup_dice_simuに代入されました。次の行をみてみましょう。

hit_rate = powup_dice_simu(small, big, n=kaisu, seed=1.5)

powup_dice_simuは、time_mesurement_decoの内部関数のwrapperであり、外側の関数 time_mesurement_decoの引数funcには関数dice_simuが渡された状態でしたね。
 これを踏まえて内部関数wrapperを見てみましょう。

   def wrapper(*args, **kwargs):

       ##引数funcに渡された関数の関数名を取得して出力
       func_name = func.__name__
       print("関数"+func_name+"シミュレーション開始...")
       
       ##実行直前のタイムスタンプを取得
       start_time = datetime.datetime.now()
       

       ##funcを実行
       result = func(*args, **kwargs)
       


       ##実行完了直後のタイムスタンプを取得し、処理時間を計算
       finish_time = datetime.datetime.now()
       processing_time = finish_time - start_time
       
       ##処理時間を出力
       print("関数"+func_name+"シミュレーション完了!!!\n")
       print("シミュレーションにかかった時間:", processing_time)
       print("\n")

       ##funcの実行結果を返す
       return result

 関数wrapperの引数を(*args, **kwargs)とすることで、関数wrapperはどんな引数をとる関数であっても良いようになっています。詳しくは「Pythonの関数-可変長の引数(*args、**kwargs)」を参照ください。
 つづいて、関数wrapperのプログラムブロックの中身をみてみましょう。まずは最初の3行(コメント文を除く)です。

       ##引数funcに渡された関数の関数名を取得して出力
       func_name = func.__name__
       print("関数"+func_name+"シミュレーション開始...")
       
       ##実行直前のタイムスタンプを取得
       start_time = datetime.datetime.now()

Pythonの関数は、

<関数の定義本体オブジェクトの変数名>.__name__

で、def宣言で定義された関数名を取得できます。ここで、引数funcには関数dice_simuが渡されていましたので、func_nameには文字列"deco_sim"が代入され、その後でシミュレーション開始の合図を出力し、タイムスタンプを取得しています。

次の行を見てみましょう。

result = func(*args, **kwargs)

となっています。ここで、funcは関数dice_simuであり、引数*argsと**kwargsは、関数wrapperの引数がそのまま関数funcに渡されています。そこで、関数wrapperのオブジェクトである 関数powup_dice_simuの呼び出しを見てみましょう。

hit_rate = powup_dice_simu(small, big, n=kaisu, seed=1.5)

引数は、small(=4), big(=6), n=kaisu(=10000000), seed=1.5の4つで、これがfunc、すなわちdice_simuの引数*args, **kwargsに渡されています。
 すなわち、関数wrapperの

result = func(*args, **kwargs)

では、

result = dice_simu(4, 6, n=10000000, seed=1.5)

が実行され、関数dice_simuのシミュレーション結果がresultに代入されました。

 関数wrapperの続きを見てみましょう。

       ##実行完了直後のタイムスタンプを取得し、処理時間を計算
       finish_time = datetime.datetime.now()
       processing_time = finish_time - start_time
       
       ##処理時間を出力
       print("関数"+func_name+"シミュレーション完了!!!\n")
       print("シミュレーションにかかった時間:", processing_time)
       print("\n")

       ##funcの実行結果を返す
       return result

func(=daice_simu)を実行した後で、タイムスタンプをとり、最初のタイムスタンプとの差分で処理時間を計算し、シミュレーションが終了したことと処理時間を出力し、dice_simuのシミュレーション結果を呼び出し元に返しています。

 さて、ずいぶん説明が長くなってしまいましたが、この方法を使うと関数dice_simuの前後に、他の関数でも使える共通的な修飾が追加されています。すなわち、関数time_mesurement_decoの引数funcにはあらゆる関数を引数として渡すことができ、関数の処理時間を計測できるようになってるのです。
 time_mesurement_decoのように、関数を修飾する関数を、デコレータと呼びます。

5.デコレーションのシンタックスシュガー

 もう少し続きがあります。Pythonでデコレータを利用するときは、慣例として、Pythonインタープリタが用意した簡易表記(シンタックスシュガー)を使って呼び出します。
 deco_ex3.pyの、デコレーションされる関数のdef文の手前に、デコレーションを定義した関数名で、

@<デコレーションを定義した関数名>
def <デコレーションされる関数>(引数, ・・・)
    <デコレーションされる関数のプログラムブロック>

のように定義すると、呼び出し部でデコレーションされる関数を呼び出しただけでdefで定義した関数が、デコレーションで定義した関数名の関数でデコレーションされます。

 説明だとわかりづらいので、deco_ex3.pyをデコレーションのシンタックスシュガーで書き換えてみます。

##deco_ex4.py

##タイムスタンプを取得する
##datetimeオブジェクトを取り込んで利用する
import datetime

##乱数を発生させるために
##randomオブジェクトを取り込んで利用する
import random


##デコレーションを定義した関数
def time_mesurement_deco(func):
   def wrapper(*args, **kwargs):
       ##引数funcに渡された関数の関数名を取得
       func_name = func.__name__
       print("関数"+func_name+"シミュレーション開始...")
       
       ##実行直前のタイムスタンプを取得
       start_time = datetime.datetime.now()
       
       ##funcを実行
       result = func(*args, **kwargs)
       
       ##実行完了直後のタイムスタンプを取得し、処理時間を計算
       finish_time = datetime.datetime.now()
       processing_time = finish_time - start_time
       
       print("関数"+func_name+"シミュレーション完了!!!\n")
       print("シミュレーションにかかった時間:", processing_time)
       
       print("\n")

       return result
   
   return wrapper
       
##1~6の整数の乱数を発生させる
##乱数がsmall以上、big以下の確率を計算して返す

## dice_simuをtime_mesurement_decoでデコレート
@time_mesurement_deco
def dice_simu(small, big, n=1000000, seed=0):
   random.seed(seed)
   
   hit_num = 0
   for i in range(n):
       deme = random.randrange(1, 7)
       
       if deme >= small and deme <= big:
           hit_num += 1
   
   hit_rate = hit_num / n
   
   return hit_rate


##プログラム実行開始位置
##シミュレーション初期値の設定
small = 4
big = 6
kaisu = 10000000


##powup_dice_simを実行すると、
##引数にdice_simuが渡された状態の関数time_mesurement_decoの
##内部関数wrapperが実行される
##関数wrpperの中のfunは関数dice_simu
hit_rate = dice_simu(small, big, n=kaisu, seed=1.5)


print("さいころをふった回数:", kaisu, "回")
print("出目が", small, "以上", big, "以下の確率:", hit_rate)

6.デコレータの作成/利用パターンまとめ

 中身のやりとりの仕組を追いかけるのはなかなか大変なですね。最後に、デコレータの作成・利用パターンを数学の公式のような感じでまとめておきます。このパターンを覚えておけば、ここまでの説明はよくわかってなくても関数の修飾ができるますので、デコレーションのひな形として利用して下さい。

##デコレータ定義
def deco(func):

    def wrapper(*args, **kwargs):

        ##<ここに関数呼び出し前の修飾を書く>##
       
        result = func(*args, **kwargs)

        ##<ここに関数呼び出し後の修飾を書く>##
       
        return result
   
    return wrapper


##関数"myfunc"にデコレータを使う
@deco
def myfunc(a, b):
    c = a + b
    return c

##関数にデコレータdecoがセットされてるので、
##myfuncを呼び出すと関数decoでデコレートされる
wa = myfunc(1, 1)

    

前の記事










 


この記事が気に入ったらサポートをしてみませんか?