一言でいうと「関数やクラスのheaderに機能を付与する」パターンです。
Pythonのビルトイン機能でサポートしていますので、これを見ていきます。
平均値を計算するmean
関数に対し、元が文字列でも平均値が計算できるようにします。
(例:["0.1", 0.2, "0.3"]
という配列でも計算できるようにする)
import functools
def main():
print(mean("0.1", 0.2, "0.3"))
# Decorateする関数
def float_args_and_return(function):
@functools.wraps(function)
def wrapper(*args, **kargs):
args = [float(arg) for arg in args]
return function(*args, **kargs)
return wrapper
# Decorateされる関数
@float_args_and_return
def mean(first, second, *rest):
numbers = (first, second) + rest
return sum(numbers) / len(numbers)
if __name__ == "__main__":
main()
まず、Decoratorを使うには以下の2点だけ抑えておけばOKです。
ここから下はDecorator関数で何が起きているか把握したい人向けです。
仕組みはどうでもよい人は飛ばしてください。
まず、関数の中に関数があります。
別途ページにも解説していますが、ここでも簡易的に説明します。
すごく簡単な例でいくと、こんな感じです。
def func1(arg1):
def func2(arg2):
print(arg1, arg2)
return func2
if __name__ == "__main__":
f2 = func1("hoge")
print(f2) # => <function __main__.func1.<locals>.func2(arg2)>
f2("hoge2") # => hoge hoge2
func1("hoge")("hoge2") # => hoge hoge2
func1
を呼ぶと、func2
が返ってきます。翻って、元々のコードを見てみます。
def float_args_and_return(function):
@functools.wraps(function)
def wrapper(*args, **kargs):
args = [float(arg) for arg in args]
return function(*args, **kargs)
return wrapper
# Decorateされる関数
@float_args_and_return
def mean(first, second, *rest):
numbers = (first, second) + rest
return sum(numbers) / len(numbers)
まず、@float_args_and_return
の2行によりmean
にfloat_args_and_return(mean())
が代入されます。
これは、Pythonの機能によるものです。
そのため、mean("0.1", 0.2, "0.3")
は float_args_and_return(mean)("0.1", 0.2, "0.3")
という意味になり、
結果としてwrapper
関数の中身を先に実現することができます。
なお、functools.wraps
のところはドキュメントなどを引き継ぐためのおまじないです。
doctestなどを行うときには必要ですが、本筋とはあまり関係ありません。
関数の引数の型をチェックするデコレータを作ります。
import functools
error_msg1 = "Decorator args len != decorated func args len"
error_msg2 = "Decorator args[{0}] type != decorated func args[{0}] type"
def main():
print(make_tagged("hello", "p"))
print(repeat("hello", 3, "|"))
print(repeat("hello", "3", "|"))
def statically_typed(*types):
def decorator(function):
@functools.wraps(function)
def wrapper(*args, **kargs):
# 本処理ここから
if len(args) != len(types):
raise ValueError(error_msg1)
for i, (arg, type_) in enumerate(zip(args, types)):
if not isinstance(arg, type_):
raise ValueError(error_msg2.format(i))
# 本処理ここまで
result = function(*args, **kargs)
return result
return wrapper
return decorator
@statically_typed(str, str)
def make_tagged(text, tag):
return "<{0}>{1}</{0}>".format(tag, text)
@statically_typed(str, int, str)
def repeat(what, count, separator):
return ((what + separator) * count)[:-len(separator)]
if __name__ == "__main__":
main()
動きとしては、先ほどと同じです。
make_tagged = statically_typed({statically_typedの引数})(make_tagged({make_taggedの引数}))
となっており、
statically_typedの引数を取り出すためにもう一段関数をかませる必要があります。
Pythonのデコレータを使うことで、簡易にDecoratorを実装することができます。
特になし
Wikipedia - Decorator
(パターン図をお借りしました)