🎃

デコレーターの基礎理解

に公開

はじめに

デコレータを書こうと思い至ることがあるのですが、仕組みを忘れていることがあるので備忘録として残します。結果だけ知りたい方は最後に完成形を載せているので、参照いただければと思います。

デコレータとは

関数に機能追加するための仕組みです。
例えば、関数の実行時間を知りたいので、以下の処理を作るとします。

import datetime

def sample_function():
    print(f"sample_function 開始時刻:{datetime.datetime.now()}")
    ##### 処理内容 #####
    print(f"sample_function 終了時刻:{datetime.datetime.now()}")

sample_function()

誰が読んでも分かりやすいですが、関数を作成する度に処理をかくのは面倒です。
これを改善してくれるのがデコレータです。

デコレータを理解するための基礎知識

デコレータを知る前に関数をオブジェクトとして使用できるという知識が重要です。

def run_func(func):
    func()

def hello():
    print("おはようございます!")

say_hello = hello
say_hello()
run_func(say_hello)

上記の結果から、関数は変数に代入したり引数として使用できることが分かります。

デコレータ

実際にデコレータを作っていきましょう。

import datetime

def print_time(func):
    def wrapper():
        print(f"開始時刻:{datetime.datetime.now()}")
        func()
        print(f"終了時刻:{datetime.datetime.now()}")
    return wrapper

def hello():
    print("おはようございます!")

say_hello = print_time(hello)
say_hello()

本処理の順序としては

  1. print_timeの関数をsay_hello変数に代入
  2. print_time関数はwrapper関数を返し、say_hello変数に代入
  3. say_helloが実行されて、wrapper関数が実行される
    です。

ただ上記を見た人はwrapper変数にfunc変数が渡されていないけど?って思う方もいるのではないでしょうか。今回ここは詳細に話しませんが、関数内で関数を定義した場合、外側の変数(今回でいうfunc変数)を記憶しています。そのためfunc関数がwrapper関数で使えています。

import datetime

def print_time(func):
    def wrapper():
        print(f"開始時刻:{datetime.datetime.now()}")
        func()
        print(f"終了時刻:{datetime.datetime.now()}")
    return wrapper

@print_time
def hello():
    print("おはようございます!")

hello()

これがデコレータです。実行して欲しい関数名も表示するなら以下のようにします。

import datetime

def print_time(func):
    def wrapper():
        print(f"{func.__name__}開始時刻:{datetime.datetime.now()}")
        func()
        print(f"{func.__name__}終了時刻:{datetime.datetime.now()}")
    return wrapper

@print_time
def hello():
    print("おはようございます!")

hello()

ただこれ、まだ欠点があります。以下のようにhello.__name__などを見た場合はどうなると思いますか。

import datetime

def print_time(func):
    def wrapper():
        print(f"{func.__name__}開始時刻:{datetime.datetime.now()}")
        func()
        print(f"{func.__name__}終了時刻:{datetime.datetime.now()}")
    return wrapper

@print_time
def hello():
    """
    挨拶をします
    """
    print("おはようございます!")

print(hello.__name__)
print(hello.__doc__)

関数名がwrapperとなったのではないでしょうか。これはhello関数がwrapper関数に覆われていることが原因です。これを解決するのにfunctoolsライブラリを使います。

import datetime
import functools

def print_time(func):
    @functools.wraps(func)
    def wrapper():
        print(f"{func.__name__}開始時刻:{datetime.datetime.now()}")
        func()
        print(f"{func.__name__}終了時刻:{datetime.datetime.now()}")
    return wrapper

@print_time
def hello():
    """
    挨拶をします
    """
    print("おはようございます!")

print(hello.__name__)
print(hello.__doc__)

最後の修正になります。上記だと引数の対応ができていません。
*args**kwargsを使用して対応します。
また値を返してあげないといけないので、returnを追加します。

import datetime
import functools

def print_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"{func.__name__}開始時刻:{datetime.datetime.now()}")
        result = func(*args, **kwargs)
        print(f"{func.__name__}終了時刻:{datetime.datetime.now()}")
        return result
    return wrapper

@print_time
def add_a_b(a:int, b:int)->int:
    print(a+b)


print(add_a_b.__name__)
print(add_a_b.__doc__)
add_a_b(3, 5)

Discussion