madokaのブログ

勉強したことのoutput先として使ってます。内容はpythonがらみが多いかもです。

pythonでデコレーターを作るときに気をつけたいこと

sphinxでドキュメントを作成しようと思ったのですが、デコレーターをつけた関数の説明が軒並みおかしいので調べたところ、functools.wrapsというものの存在を知ったので記事にします。

デコレーターの書き方

pythonではデコレーターは以下のように作ります。

def my_decorator(func):
    """
    デコレート対象の関数の前にstart、後にendを標準出力する関数を返します。

    :param func: デコレート対象の関数
    :return: デコレートされた関数
    """
    def wrapper(*args, **kwargs):
        """
        デコレート対象の関数の前にstart、後にendを標準出力します。

        :param args:
        :param kwargs:
        :return:
        """
        print("start")
        func(*args, **kwargs)
        print("end")
    return wrapper


@my_decorator
def my_method(val1, val2):
    """
    val1, val2を標準出力します。

    :param val1:
    :param val2:
    :return:
    """
    print(val1, val2)


if __name__ == '__main__':
    my_method(1, 2)

これを実行すると以下が出力されます。

start
1 2
end

このようにmy_method前後で処理をするという目的ではこれだけも良いのですが、logやdocを使うときに問題があるのです。。

問題点

例えば、my_methodのdocstringを見てみましょう。print(my_method.__doc__)で表示させてみると以下が出てきました。

    デコレート対象の関数の前にstart、後にendを標準出力します。

    :param args:
    :param kwargs:
    :return:

my_decorator内のwrapperに書いたはずの記述が出てきてしまいました。 これはmy_methodをmy_decoratorでデコレートしたため、pythonからはwrapperという関数を実行しているようにしか見えないという状況ということかと考えられます。

もちろんpythonではこれを回避するための仕組みが用意されています。

改善策: functools.wraps

https://docs.python.org/ja/3/library/functools.html#functools.wraps

functools.wrapsが以上のことを回避するpythonで用意されている関数になります。使い方はとっても簡単です。 先ほどの例に追加してみます。

from functools import wraps


def my_decorator(func):
    """
    デコレート対象のメソッドの前にstart、後にendを標準出力するメソッドを返します。

    :param func: デコレート対象のメソッド
    :return: デコレートされたメソッド
    """

    @wraps(func) # 追加部分
    def wrapper(*args, **kwargs):
        """
        デコレート対象のメソッドの前にstart、後にendを標準出力します。

        :param args:
        :param kwargs:
        :return:
        """
        print("start")
        func(*args, **kwargs)
        print("end")

    return wrapper


@my_decorator
def my_method(val1, val2):
    """
    val1, val2を標準出力します。

    :param val1:
    :param val2:
    :return:
    """
    print(val1, val2)


if __name__ == '__main__':
    my_method(1, 2)

print(my_method.__doc__)で表示させてみると以下が出てきました。

    val1, val2を標準出力します。

    :param val1:
    :param val2:
    :return:

これは確かにmy_methodに書いたdocstringです。 ということで、pythonでデコレータを使うのは良いけどfunctools.wrapsも忘れずに!というお話でした。