Pythonのデコレータとは何か
1.デコレータとは
1-1.役割
デコレータの役割とは、関数やクラスの中身を書き換えずに処理を追加することである。デコレータは関数であり、引数として関数やクラスを受け取り、関数やクラスで処理を返す。
1-2.具体例1
次の関数に処理を追加する場合を想定する。
def hello():
print('Hello')
hello()
出力されるHello前後に「----」を加える場合、下記のようなコードが書ける
def print_line():
print('----------')
hello()
print('----------')
def hello():
print('Hello')
# def hello():
# print('----------')
# print('Hello')
# print('----------')
# hello()
print_line()
ただし、print_line関数はデコレータではない。デコレータを新しく作る場合、関数を受け取り、関数で返す必要がある。
下記のように装飾したい関数を@print_lineと定義する。
def print_line(func):
def wrapper():
print('----------')
func()
print('----------')
return wrapper
@print_line
def hello():
print('Hello')
hello()
- hello()が実行されると、def hello()が処理される。
- def hello()はprint_line関数を装飾するので、def print_lineで処理が始まる
- def print_lineは引数としてfuncが定義されている。
- このfuncが、引数としてdef hello()を受け取るため、出力結果は、以下の通りとなる
$ python hello.py
----------
Hello
----------
1-3.具体例2
下記の流れで実行される。
- myfunc("Blabla")が処理される。
- 上記は、def myfunc関数のprintにて出力処理される。
- def myfunc関数はsample_decoratorにデコレートされるため、def sample_decoratorが処理される。
- ここでdef myfunc関数が引数のmyfuncとして受け取られる。
- myfunc関数は、print(text)という処理があり、そこではBlablaが出力される。
- その結果、一番下の実行結果が出力される。
コード
def sample_decorator(myfunc):
def inner_func(*args):
print("I am the decorator!")
myfunc(*args)
return inner_func
@sample_decorator
def myfunc(text):
print(text)
myfunc("Blabla")
実行結果
I am the decorator!
Blabla
参考:複数の引数を渡したい場合、下記の実装も可能
def sample_decorator(myfunc):
def inner_func(*args):
print("I am the decorator!")
myfunc(*args)
return inner_func
@sample_decorator
def myfunc(text, text2):
print(text)
print(text2)
myfunc("Blabla", "test")
1-4.具体例3
コード
def sample_decorator(myfunc):
def inner_func(a,b):
return "I am the decorator!" + " " + a + " " + b
return inner_func
@sample_decorator
def myfunc(text):
return text
print(myfunc("test1","test2"))
出力結果
I am the decorator! test1 test2
下記に処理の流れを記載する。
- まず、print(myfunc("test1","test2"))が実行される。
- 実行されたmyfuncを参照するため、def myfunc(text):が実行される。
- de myfunc(text)はsample_decoratorにデコレートされるため、def sample_decorator(myfunc)が実行される。
- その中のinner_funcは、変数aとbを持つ。最初にmyfunc("test1","test2")と変数を代入しているため、最終的にこれらが変数として出力される。
2.デコレータの応用的な使い方
2-1.元の関数のメタデータを維持する
先ほどまで説明したデコレータの定義だと、元の関数のメタデータが、デコレータで使用された関数のメタデータにおきかわってしまった。元の関数のメタデータを維持するため、nameメソッドで元の関数名をチェックする
def sample_decorator(myfunc):
def inner_func():
return "I am the decorator!"
return inner_func
@sample_decorator
def myfunc(text):
return text
print(myfunc.__name__)
実行結果
inner_func
本来、myfuncと表示したいが、元データのinner_funcが表示される。myfuncのメタデータに変更を加えたくない場合、functools.wrapsにより解決できる。functools.wrapsは、元の関数のメタデータに変更を加えないためのデコレータである。inner_funcの前に変更を加えたくない関数(myfunc)を引数として渡すことで、メタデータの変更をしないように処理できる。
from functools import wraps
def sample_decorator(myfunc):
@wraps(myfunc)
def inner_func():
return "I am the decorator!"
return inner_func
@sample_decorator
def myfunc(text):
return text
print(myfunc.__name__)
実行結果
myfunc
2-2.複数のデコレータを使う
下記の流れで処理が行われる。
- まずdef myfuncに近い順で処理されるため、@A→@Bという順になる
- @Aは下記の通り、出力される。
- I am the A decorator!
- Hello, decorator
- I am the A decorator!
- 次に、@Bが出力される。
- I am the B decorator!
- (Aの実行結果)
- I am the B decorator!
- (Aの実行結果)は、上記出力結果で代入されるため、一番下の実行結果が出力される。
def A(myfunc):
def inner_func():
print("I am the A decorator!")
myfunc()
print("I am the A decorator!")
return inner_func
def B(myfunc):
def inner_func():
print("I am the B decorator!")
myfunc()
print("I am the B decorator!")
return inner_func
@B
@A
def myfunc():
print("Hello, decorator")
myfunc()
実行結果
I am the B decorator!
I am the A decorator!
Hello, decorator
I am the A decorator!
I am the B decorator!
3.Pythonのデコレータにwrapsを使うべき理由
「2-1.元の関数のメタデータを維持する」にて、下記事例を確認した。
- @wrapsをつけずにprint(myfunc.name)を実行するとinner_funcが表示される。
- @wrapsをつけてprint(myfunc.name)を実行するとmyfuncが表示される。
上記の何が問題かというと、実際動かしている関数と関数名が一致していないため、doctestが動かなくなる、という事態が発生する(*通常動作では問題ない)。別事例で説明しよう。
def hoge_decorator(f):
def hoge_wrapper(*args, **kwargs):
"""デコレータのDocstringだよ"""
print("デコレータだよ")
return f(*args, **kwargs)
return hoge_wrapper
@hoge_decorator
def hoge_function():
"""デコってる関数のDocstringだよ"""
print("これがデコってる関数だ!")
if __name__ == '__main__':
hoge_function()
結果
デコレータだよ
これがデコってる関数だ!
上記は、問題なく動作する。ただ、下記print出力を加えるとどうなるか。
def hoge_decorator(f):
def hoge_wrapper(*args, **kwargs):
"""デコレータのDocstringだよ"""
print("デコレータだよ")
return f(*args, **kwargs)
return hoge_wrapper
@hoge_decorator
def hoge_function():
"""デコってる関数のDocstringだよ"""
print("これがデコってる関数だ!")
if __name__ == '__main__':
hoge_function()
print(hoge_function.__name__)
print(hoge_function.__doc__)
結果
デコレータだよ
これがデコってる関数だ!
hoge_wrapper
デコレータのDocstringだよ
nameはhogewrapperだが、docはhoge_functionのデコレータを出力するため、実際に動かしている関数が、意図したものとは異なるという事態になる。そのため、通常動作では問題ないが、doctestだと動かなくなってしまう。
参考:doctestとは
doctestとは、簡易的なテストを実行するためのpython標準ライブラリである。下記の通り、docstringの実行内容と正しい返り値をセットで書くことにより、テストを実行できる。テストは、「python hoge.py」で実行できる。
def add(a, b):
'''
>>> add(1, 2)
3
>>> add(-8, -2)
-10
'''
pass
if __name__ == '__main__':
import doctest
doctest.testmod()
実行結果
**********************************************************************
File "__main__", line 3, in __main__.add
Failed example:
add(1, 2)
Expected:
3
Got nothing
**********************************************************************
File "__main__", line 5, in __main__.add
Failed example:
add(-8, -2)
Expected:
-10
Got nothing
**********************************************************************
1 items had failures:
2 of 2 in __main__.add
***Test Failed*** 2 failures.
TestResults(failed=2, attempted=2)
上記の通り、エラーが発生する。return a+bとしていなかったことが原因なので、コードを修正する。
def add(a, b):
'''
>>> add(1, 2)
3
>>> add(-8, -2)
-10
'''
return a + b
if __name__ == '__main__':
import doctest
doctest.testmod()
正しくテストが実行される。テストが全て成功すれば、出力はされない。
参考文献
Discussion