pikesaku’s blog

個人的な勉強メモです。記載内容について一切の責任は持ちません。

Pythonの関数デコレータについて

関数デコレータとは?

関数に機能を追加する機能

ポイント

Pythonの以下性質を利用
・関数もオブジェクト
・関数自体を引数や戻り値として利用可能

利用イメージは以下の通り
・関数Aに引数として、関数Bを指定
・戻り値として、関数Bの機能に関数Aの機能を追加した関数を返す。(関数宣言時点で動作する。※後述)

コードのイメージ
関数base_funcに機能を追加する場合、以下の記述が可能

base_func = add_func(base_func)


このシンタックスシュガー(簡略記述)が関数デコレータ。以下記述が可能。

@add_func
def base_func():
    処理


サンプルコード

関数デコレータなしで記述した場合


コード

def add_func(f):
    def func():
        f()
        print("add func called")
    return func


def base_func():
    print("base func called")


print("# 機能追加前")
base_func()

print("# 機能追加後")
base_func = add_func(base_func)
base_func()


実行結果

# 機能追加前
base func called
# 機能追加後
base func called
add func called


関数デコレータで記述した場合


コード

## デコレータありの場合

def add_func(f):
    def func():
        f()
        print("add func called")
    return func


@add_func
def base_func():
    print("base func called")


base_func()


実行結果

base func called
add func called

着目すべき動作

コード

def add_func(f):
    print("test")
    def func():
        f()
        print("add func called")
    return func


@add_func
def base_func():
    print("base func called")


実行結果

test

★重要。おさえるべき動き★

base_funcは実行してないが、testが出力される。

base_func定義が宣言された時点で、add_funcが実行され、add_funcのreturnが、base_funcとして登録される。

 

サンプルコードの問題点


オブジェクトの属性情報(関数名等)が変わってしまう。

さっきのコードの最後の行に以下を追加すると、、、

print(base_func.__name__)


実行結果は以下になる。

base func called
add func called
func


関数名が、add_func内側で定義した関数名になっている。
これを回避するには、func関数をfunctoolsモジュールのwraps関数でデコレートする。
 
コード

from functools import wraps


def add_func(f):
    @wraps(f)
    def func():
        f()
        print("add func called")
    return func


@add_func
def base_func():
    print("base func called")


base_func()
print(base_func.__name__)


実行結果

base func called
add func called
base_func


引数付き関数のデコレータ


コード

rom functools import wraps

def add_func(f):
    @wraps(f)
    def func(a):
        f(a)
        print("add func called")
    return func


@add_func
def base_func(a):
    print("base func called " + a)


base_func("hoge")
print(base_func.__name__)


実行結果

base func called hoge
add func called
base_func


おぼえておくべき点


・base_funcを実行すると、add_funcの第一引数(f)としてbase_funcが渡される。
・base_func実行時の引数は、add_func内で定義した関数(func)の引数(a)にセットされる。

デコレータ記述(@add_func)に引数をつける事も可能


add_func関数内でネストする必要あり。コードは以下の通り。

コード

from functools import wraps


def add_func(b):
    def func1(f):
        @wraps(f)
        def func2(a):
            print(b)
            f(a)
            print("add func called")
        return func2
    return func1


@add_func("moge")
def base_func(a):
    print("base func called " + a)


base_func("hoge")
print(base_func.__name__)


実行結果

moge
base func called hoge
add func called
base_func

base_funcはadd_funcデコレータでfunc1で登録されるが、func1はfunc2をreturnする。
base_fund実行時は、結局func2が実行される。

最後に


・参考URLに記載の通りビジネスロジックと管理ロジックを分けるために有用
・ただ、自分はインフラSEだし、そんな本格的なコードは書いてません。。。。。関数に渡されたオブジェクトをデバッグとしてログに出したい時などに使えるかも

サンプルコード

def deco(f):
    def func(*args):
        print("#DEBUG " + f.__name__ + " args")
        print(args)
        f(*args)
    return func

@deco
def test1(a, b):
    print("HOGE!!")

@deco
def test2(b):
    print("MOGE!!")


def main():
    test1(1,2)
    test2(3)

main()


実行結果

#DEBUG test1 args
(1, 2)
HOGE!!
#DEBUG test2 args
(3,)
MOGE!!