Decoratorパターン

概要

一言でいうと「関数やクラスの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()

# 出力

0.20000000000000004

解説

まず、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が返ってきます。
  • 関数は変数に代入することもできますし(1行目)、もちろん直接呼んでもOKです(4行目)。
  • 関数の中で定義された関数は外の関数のことを知っています(ここではarg1が未定義でないということ)。 - これはクロージャと呼ばれています。

翻って、元々のコードを見てみます。

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行によりmeanfloat_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()

# サンプルコード② - 出力

<p>hello</p> hello|hello|hello ... ## エラー出力 ValueError: Decorator args[1] type != decorated func args[1] type

解説

動きとしては、先ほどと同じです。
make_tagged = statically_typed({statically_typedの引数})(make_tagged({make_taggedの引数}))となっており、
statically_typedの引数を取り出すためにもう一段関数をかませる必要があります。

Pythonらしく書くためのコツ

Pythonのデコレータを使うことで、簡易にDecoratorを実装することができます。

クラス図

# 一般的なクラス図

一般的なクラス図(Decoratorパターン)

# サンプルコードにおけるクラス図

特になし

参考URL