Skip to content

Python でデコレータを利用する

Python のデコレータ (decorator) は 公式ドキュメントの用語集 で以下のように定義されています。

Note

(デコレータ) 別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod() と staticmethod() です。

デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:

def f(arg):
    ...
f = staticmethod(f)

@staticmethod
def f(arg):
    ...

同じ概念がクラスにも存在しますが、あまり使われません。デコレータについて詳しくは、 関数定義 および クラス定義 のドキュメントを参照してください。

つまり、デコレータの実態は「関数を受け取り、関数を返す関数」と言えます。 もう少し分かりやすく噛み砕くと「関数の処理内容を変更せず、関数に機能を追加出来る仕組み」とも言えます。

関数も「オブジェクト」

Python では関数も「オブジェクト」として扱うことが出来ます。 例えば以下のソースコードを考えます。

1
2
3
4
def greeting():
    print("Hello, World")

greeting()

これは以下のように書き換えることが出来ます。

1
2
3
4
5
def greeting():
    print("Hello, World")

v = greeting
v()

4 行目で v = greeting と変数 vgreeting を代入しています。 代入時は「関数を呼び出しているわけでは無い」為、括弧は指定しません。 5 行目で関数を実行していますが、このタイミングで括弧を指定します。 上記、いずれのソースコードも実行結果は以下です。

Hello, World!

関数内関数

Python では関数の中に、更に関数を定義することが出来ます。 これは公式ドキュメントの 8.7. 関数定義 で言及されており、一般には Inner Function や Nested Function と呼ばれています。 下記のソースコードでは 2 つの関数を定義しています。

timestamp 関数

関数を引数として受け取り、変数 f に格納する。 関数内関数を用いて、関数 f の実行前後で datetime.datetime.now() を実行し、現在時刻を表示する。

greeting 関数

Hello, World という文字列を表示する

14 行目で timestamp(greeting)() を実行していますが、これは「timestamp 関数に対して、引数として greeting 関数を指定する。 greeting 関数の引数は () (=無し)」という意味になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import datetime


def timestamp(f):
    def wrapper(*args, **keywords):
        print(datetime.datetime.now())
        f(*args, **keywords)
        print(datetime.datetime.now())
    return wrapper

def greeting():
    print("Hello, World")

timestamp(greeting)()
2023-12-02 21:00:59.568289
Hello, World
2023-12-02 21:00:59.568317

デコレータを使った書き換え

前のサンプルのように「関数の引数に・関数を指定する」実行方法はやや分かりづらいかも知れません。 このような場合はデコレータを使うことでやや見通しが良くなります。 デコレータは関数名の前に @関数名 を指定することで定義出来ます。 下記では 14 行目の greeting 関数に対して、13 行目で @timestamp デコレータを指定しています。 この指定により、「timestamp 関数の引数として greeting 関数を呼び出す」という振る舞いをします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import datetime


def timestamp(f):
    def wrapper(*args, **keywords):
        print(datetime.datetime.now())
        f(*args, **keywords)
        print(datetime.datetime.now())
    return wrapper

@timestamp
def greeting():
    print("Hello, World")

greeting()

実行結果は以下です。 「デコレータを使わないケース」「デコレータを使ったケース」いずれも本質的には同じ意味になり、実行結果は同じです。

2023-12-02 21:01:00.770590
Hello, World
2023-12-02 21:01:00.770623

返り値を持つメソッドをデコレーションする

返り値を持つメソッドをデコレーションする場合、デコレータ関数側で値を return するようにします。 下記のサンプルコードでは 3 行目で「f 関数 (実態は greeting() で得られた値の最後に ! を追加」し、それを 4 行目で return しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def exclamate(f):
    def wrapper(*args, **keywords):
        msg = f(*args, **keywords) + "!"
        return msg
    return wrapper

@exclamate
def greeting(message: str):
    return message

msg = greeting("Hello, World")
print(msg)

実行結果は以下の通りです。 11 行目では文字列 Hello, World を指定していますが、デコレータによって処理された結果、! の文字列が追加され、結果が Hello, World! になっていることが分かります。

Hello, World!

メタデータを保持する

下記のコードでは 11 行目で greeting 関数の __name__ を表示しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def exclamate(f):
    def wrapper(*args, **keywords):
        msg = f(*args, **keywords) + "!"
        return msg
    return wrapper

@exclamate
def greeting(message: str):
    return message

print(greeting.__name__)

しかし、結果は (greeting では無く) デコレーション関数内関数名である wrapper が得られます。 これは本来得たかった値ではありません。

wrapper

これを解消するには @functools.wraps デコレータを使います。 公式ドキュメントでは以下のように記載されています。

Note

これはラッパー関数を定義するときに update_wrapper() を関数デコレータとして呼び出す便宜関数です。これは partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated) と等価です。例えば:

>>>
from functools import wraps
def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwds):
        print('Calling decorated function')
        return f(*args, **kwds)
    return wrapper

@my_decorator
def example():
    """Docstring"""
    print('Called example function')

example()
Calling decorated function
Called example function
example.__name__
'example'
example.__doc__
'Docstring'
このデコレータ・ファクトリを使用しないと、上の例中の関数の名前は 'wrapper' となり、元の example() のドキュメンテーション文字列は失われてしまいます。

下記では 5 行目で関数内関数 wrapper@functools.wraps(f) でデコレートしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import functools


def exclamate(f):
    @functools.wraps(f)
    def wrapper(*args, **keywords):
        msg = f(*args, **keywords) + "!"
        return msg
    return wrapper

@exclamate
def greeting(message: str):
    return message

print(greeting.__name__)

実行結果は下記です。 これで greeting が得られるようになりました。

greeting