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


サンプルコードの問題点


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

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

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


最後に


・参考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!!


最後に2


参考URLの以下記述について

以下はよくない

def web_lookup(url, saved={}):
    if url in saved:
        return saved[url]
    page = urllib.urlopen(url).read()
    saved[url] = page
    return page


以下がよい

def cache(func):
    saved = {}
    @wraps(func)
    def newfunc(*args):
        if args in saved:
            return saved[args]
        result = func(*args)
        saved[args] = result
        return result
    return newfunc

@cache
def web_lookup(url):
    return urllib.urlopen(url).read()


と記述があったが、"return urllib.urlopen(url).read()"が理解できなかった。このコードは実行されないのでは?と思った。理由は、newfuncでreturnを定義している為。ただ、よくよく考えるとresult = func(*args)で実行される。すっきり!