Python
Python3
decorator

Python デコレータ再入門  ~デコレータは種類別に覚えよう~

Pythonのアットマーク(@)を使った記法であるデコレータについてまとめる。難しいと思われているかもしれないデコレータだが、デコレータの種類(パターン)を意識することが理解の近道だと思ったので今回は種類別にデコレータの簡単な実装例をあげて解説をしていこうと思う。


対象者


  • Python のデコレータを学んだことがあるがたまに分からなくなる

  • デコレータを自作しろと言われたらスラスラかけるか不安


知っていると良いこと


  • 関数のスコープ

  • 第一級関数


  • *args , **kwargs のような記法


デコレータ関数の種類分け

以下2つの要因でデコレータ関数の種類分けが可能だと思われる。


  • 引数を取るか否か

  • ラッパー関数を返すか否か

よって組み合わせから計4つのパターンが考えられるが、今回は "引数を取らずラッパー関数を返さないデコレータ"を除いた3つを紹介する。1つ除外する理由は簡単すぎて意味がなさそうだから。

用語の定義として、デコレータを乗せている関数( @decorator の下に定義する関数)を 元の関数 と呼ぶことにする(良い名前ありますか?)


1. 引数無しデコレータでラッパー関数を返す場合

def args_logger(f):

def wrapper(*args, **kwargs):
f(*args, **kwargs)
print('args: {}, kwargs: {}'.format(args, kwargs))
return wrapper

@args_logger
def print_message(msg):
print(msg)

# 以下と等価
'''
def print_message(msg):
print(msg)
print_message = args_logger(print_message)
'''

print_message('hello')

実行結果

hello

args: ('hello',), kwargs: {}

まず1番理解しやすいものから。 args_logger 関数は元の関数の引数情報をprint()するラッパー関数を返す。

つまり @args_logger を乗っけた関数は実行の度に引数情報を吐くようになる。

この例のように、返すラッパー関数は wrapper という関数名にするのが慣例である。


2. 引数ありデコレータでラッパー関数を返さない場合

funcs = []

def appender(*args, **kwargs):
def decorator(f):
# args や kwargsの内容によって処理内容を変えたり変えなかったり
print(args)
if kwargs.get('option1'):
print('option1 is True')

# 元の関数をfuncsに追加
funcs.append(f)
return decorator

@appender('arg1', option1=True)
def hoge():
print('hoge')

@appender('arg2', option2=False)
def fuga():
print('fuga')

# 以下と等価
'''
def hoge():
print('hoge')
appender('arg1', option1=True)(hoge)

def fuga():
print('fuga')
appender('arg2', option2=False)(fuga)
'''

for f in funcs:
f()

実行結果

('arg1',)

option1 is True
('arg2',)
hoge
fuga

appender デコレータは元の関数をfuncsリストにappendする。デコレータ関数に渡された引数によって処理内容を変えたりできる(今回は良い例を思いつかなかったので適当にprintしているだけ)。

注意する点は、デコレータに引数を保つ場合、デコレータ関数内では直接元の関数 を扱えないということだ。代わりに「元の関数を扱う関数」 を定義する。この関数名は慣例として decorator という名前を付ける(多分)。

このパターンのデコレータとしてはflaskの app/Flask.route 関数が当てはまる。

https://github.com/pallets/flask/blob/3a0ea726bd45280de3eb3042784613a676f68200/flask/app.py#L1222

flaskのように単にコールバック関数を定義するためにこのパターンは使われると思われる。


3. 引数ありデコレータでラッパー関数を返す場合

def args_joiner(*dargs, **dkwargs):

def decorator(f):
def wrapper(*args, **kwargs):
newargs = dargs + args # リストの結合
newkwargs = {**kwargs, **dkwargs} # 辞書の結合 (python3.5以上で動く)
f(*newargs, **newkwargs)
return wrapper
return decorator

@args_joiner('darg', dkwarg=True)
def print_args(*args, **kwargs):
print('args: {}, kwargs: {}'.format(args, kwargs))

# 以下と等価
'''
def print_args(*args, **kwargs):
print('args: {}, kwargs: {}'.format(args, kwargs))
print_args = args_joiner('darg', dkwarg=True)(print_args)
'''

print_args('arg', kwarg=False)

実行結果

args: ('darg', 'arg'), kwargs: {'kwarg': False, 'dkwarg': True}

1番目の例と2番目の例の合わせ技のような感じでさらに複雑になった。

args_joiner デコレータは、元の関数の引数とデコレータの引数を連結させた引数を取る関数を返す関数を返す(引数を連結させるという処理に深い意味はない、ただの例)。「関数を返す関数を返す」と一段回ネストが深くなったことに注意。処理をがんばって追えば何をしているのか分かると思う。


どの種類のデコレータなのか見抜けるようになろう

3つの例を通してまとめ。

デコレータが何をしているか理解するために、どの種類のデコレータなのかを見抜ければ後は楽である。

例えば一番目の例のように

def hoge_deco(func):

def wrapper(...):
...
return wrapper

このように書かれていたら、 hoge_deco デコレータは 引数を取らない デコレータで ラッパー関数を返す やつだと一瞬でわかると良い。判断材料は hoge_deco の引数に func を取っていることと、 wrapper 関数を定義してそれを最後にreturn していることである。

2番目の例のように

def fuga_deco(*args, **kwargs):

def decorator(f):
# args, kwargs, fを使って何かする(fをあるリストに加えるなど)
...
return decorator

このように書かれていたら、 fuga_deco デコレータは 引数を取る デコレータで ラッパー関数を返さない やつだと思おう。判断材料は fuga_deco の引数に関数以外の何かを取っていることと、 wrapper という名前ではなく、 関数 f を引数に取る decorator という名前の関数が定義されていてそれを返していることである。このパターンのデコレータはラッパー関数を返さないので元の関数自体はなんの変化もないことに注意。

3番目の例のように

def piyo_deco(*dargs, **dkwargs):

def decorator(f):
...
def wrapper(*args, **kwargs):
...
return wrapper
return decorator

と書かれていたら、 piyo_deco デコレータは 引数を取る デコレータで ラッパー関数を返す やつだと思おう。判断材料は、、、もういいか。

この例のデコレータは例えばどのような物がありますかね。ちょっと思いつきませんがけっこうあると思います。


デコレータをスラスラ実装できるようになろう

デコレータのコードが理解できれば、逆にデコレータを自分で作ることも楽になってくるはず。今回の3つのどれかのパターンで殆ど書けると思うので、まずはどのパターンを必要としているか考えて、後は機械的に *arg, **args とか def wrapper とか def decorator とか書けるようになればあなたはデコレータマスターと言えましょう。

僕自身デコレータを自分で作ったことはありませんが、この記事を書くことで良い復習になりました。これからバンバンデコレートしていきたい所存!