Python のデコレータをクラスで実装する

クラスを使ったデコレータの実装方法を紹介する。 クラスを使ってデコレータを実装する場合、__init__() メソッドの引数でデコレート対象の関数(メソッド)を受け取る。 関数に適用するデコレータとメソッドに適用するデコレータは実装方法が異なる。 関数に適用するデコレータは必ずしもクラスで実装する必要はないが、ここではクラスを使った実装を取りあげる。

関数に適用するデコレータ

関数に適用するためのデコレータをクラスで実装するには、__call__() メソッドを実装して呼び出し可能オブジェクトをラッパー関数として扱う。 デコレータの実装例を次に示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from functools import update_wrapper


class decorate_function:
    """Decorator for function"""

    def __init__(self, func):
        self.func = func
        update_wrapper(self, self.func)

    def __call__(self, *args, **kwargs):
        print(f"Decorated by {self.__class__.__name__}")
        return self.func(*args, **kwargs)

デコレータを定義した際は、デコレート後の関数の __name____doc__ が正しく表示されるように functools.update_wrapper()functools.wraps() で更新するが、 上記のように呼び出し可能オブジェクトとしてデコレータを実装した場合は __init__() の中で self を引数として update_wrapper() を呼び出せばよい。

定義したデコレータを関数に適用する次のコードは次のようになる。

1
2
3
@decorate_function
def func():
    pass

これは次のコードと等価。

1
2
3
4
def func():
    pass

func = decorate_function(func)

decorate_function クラスのイニシャライザにデコレート対象の関数が渡される。 decorate_function(func) は呼び出し可能オブジェクトを返すので、 func()decorate_function.__call__() が呼び出されるようになる。

定義したデコレータを使用するサンプルコードを次に示す。

1
2
3
4
5
6
7
8
@decorate_function
def greet(name: str):
    """Print a greeting message"""
    print(f"Hello, {name}!")


greet("World")
print(f"greet.__doc__: {greet.__doc__}")

実行結果は次の通り。

1
2
3
4
$ python sample.py
Decorated by decorate_function
Hello, World!
greet.__doc__: Print a greeting message

print(f"greet.__doc__: {greet.__doc__}") もデコレータの docstring ではなく、もとの関数の docstring が出力できている。

メソッドに適用するデコレータ

メソッドに適用するデコレータは、__init__() でデコレート対象を受け取るのは同じだが __call__() メソッドは利用しない。 呼び出し可能オブジェクトとして実装するのではくディスクリプタ(Descriptor)として実装する。 関数ではなくメソッドをデコレートする場合、メソッドがどのオブジェクトに対して呼び出されたのかを知る必要があり、 そのためにディスクリプタの __get__() を利用する。 __get__() を使った実装は Python 標準ライブラリの functools.singledispatchmethod デコレータ で使われている。 ディスクリプタの詳細についてはこちらが参考になる。 以降の説明はディスクリプタについての知識があることが前提になる。

メソッドに適用するデコレータは次のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from functools import wraps


class decorate_method:
    """Decorator for method"""

    def __init__(self, method):
        if not callable(method) and not hasattr(method, "__get__"):
            raise TypeError(f"{method!r} is not callable or a descriptor")

        self.method = method

    def __get__(self, obj, cls=None):
        print(f"decorate_method.__get__ is called from {obj}")

        if obj is None:
            return self

        print(f"decorate_method.__get__ create wrapper for {obj}")

        @wraps(self.method)
        def wrapper(*args, **kwargs):
            print(f"Decorated by {self.__class__.__name__}")
            return self.method.__get__(obj, cls)(*args, **kwargs)

        return wrapper

__get__() 内で定義している wrapper() では、 デコレート対象の関数を呼び出すのに self.method.__get__(obj, cls)(*args, **kwargs) としている。 __get__() の引数 obj は、デコレートされたメソッドを呼び出すオブジェクトになる。 decorate_method.__init__() で渡される method はオブジェクトに束縛されていないメソッドなので、 呼び出す前に self.method.__get__(obj, cls) でメソッドを呼び出したオブジェクトに束縛している。

メソッドの __get__() でオブジェクトが束縛される様子を Python インタープリタで確認してみる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> class Sample:
...     def method(self):
...         print("Hello, World!")
...

>>> Sample.method
<function Sample.method at 0x7fe22f7260e0>

>>> s = Sample()
>>> s.method
<bound method Sample.method of <__main__.Sample object at 0x7fe22f7079a0>>

>>> Sample.method.__get__(s, Sample)
<bound method Sample.method of <__main__.Sample object at 0x7fe22f7079a0>>

>>> s.method()
Hello, World!

>>> Sample.method()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Sample.method() missing 1 required positional argument: 'self'

>>> Sample.method.__get__(s, Sample)()
Hello, World!

Sample.method では function と表示されていて、束縛(bound)されていない関数が返っている。 s = Sample(); s.method では bound method となっており、オブジェクトに束縛されているのがわかる。 束縛されたオブジェクトの情報も出力されている。 次に Sample.method.__get__(s, Sample) とすると、s.method と同じ出力になっており、 オブジェクトに束縛されているのがわかる。

s.method() は成功して Sample.method() は当然エラーになるが、 Sample.method.__get__(s, Sample)() はオブジェクト s に束縛されたメソッドの呼び出しとなり、 s.method() と同じ結果が得られる。 このようにして、受け取ったクラスのメソッドを後からオブジェクトに束縛して呼び出すことができる。

先に定義したデコレータの self.method.__get__(obj, cls)(*args, **kwargs) 部分も、 デコレートする関数 self.method に、メソッドを呼び出したオブジェクト obj を束縛して呼び出している。

定義したデコレータを使用するサンプルコードを次に示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Sample:
    def __init__(self, value):
        self.value = value

    @decorate_method
    def method(self, arg: str):
        """Sample method"""
        print(f"self.value: {self.value}, arg: {arg}")


a = Sample("A")
b = Sample("B")

print('=== call a.method("one")')
a.method("one")
print()

print('=== call b.method("one")')
b.method("one")
print()

print('=== call a.method("two")')
a.method("two")
print()

実行結果は次のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ python sample.py
=== call a.method("one")
decorate_method.__get__ is called from <__main__.Sample object at 0x7f7fabcfd570>
decorate_method.__get__ create wrapper for <__main__.Sample object at 0x7f7fabcfd570>
Decorated by decorate_method
self.value: A, arg: one

=== call b.method("one")
decorate_method.__get__ is called from <__main__.Sample object at 0x7f7fabccb430>
decorate_method.__get__ create wrapper for <__main__.Sample object at 0x7f7fabccb430>
Decorated by decorate_method
self.value: B, arg: one

=== call a.method("two")
decorate_method.__get__ is called from <__main__.Sample object at 0x7f7fabcfd570>
decorate_method.__get__ create wrapper for <__main__.Sample object at 0x7f7fabcfd570>
Decorated by decorate_method
self.value: A, arg: two

少しわかりにくいかもしれないが、デコレートされたメソッドの呼び出すたびに decorate_method.__get__() が呼び出されて、 その度にラッパー関数が作成されている。

メソッドに適用するデコレータ(memoize バージョン)

メソッドに適用するデコレータの最後で見たように、 先ほどの実装ではメソッド呼び出しの度にラッパー関数が作成されている。 デコレータの目的にもよると思うが、基本的にはオブジェクト毎にラッパー関数を作成すれば十分なはず そこでオブジェクト毎に1つのラッパー関数を作成して再利用するように実装する。

メモ化するには1度作成したラッパー関数を、呼び出し元のオブジェクトをキーにして WeakKeyDictionary に保存するだけ。 コードは次のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from functools import wraps
from weakref import WeakKeyDictionary


class decorate_method_memoize:
    """Decorator for method, memoize version"""

    def __init__(self, method):
        if not callable(method) and not hasattr(method, "__get__"):
            raise TypeError(f"{method!r} is not callable or a descriptor")

        self.method = method
        self.cache = WeakKeyDictionary()

    def __get__(self, obj, cls=None):
        print(f"decorate_method_memoize.__get__ is called from {obj}")

        if obj is None:
            return self

        if (wrapper := self.cache.get(obj, None)) is not None:
            return wrapper

        print(f"decorate_method_memoize.__get__ create wrapper for {obj}")

        @wraps(self.method)
        def _wrapper(*args, **kwargs):
            print(f"Decorated by {self.__class__.__name__}")
            return self.method.__get__(obj, cls)(*args, **kwargs)

        self.cache[obj] = _wrapper

        return _wrapper

定義したデコレータを使用するサンプルコードを次に示す。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Sample:
    def __init__(self, value):
        self.value = value

    @decorate_method_memoize
    def method(self, arg: str):
        """Sample method"""
        print(f"self.value: {self.value}, arg: {arg}")


a = Sample("A")
b = Sample("B")

print('=== call a.method("one")')
a.method("one")
print()

print('=== call b.method("one")')
b.method("one")
print()

print('=== call a.method("two")')
a.method("two")
print()

実行結果は次のようになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ python sample.py
=== call a.method("one")
decorate_method_memoize.__get__ is called from <__main__.Sample object at 0x7f2d61cb7430>
decorate_method_memoize.__get__ create wrapper for <__main__.Sample object at 0x7f2d61cb7430>
Decorated by decorate_method_memoize
self.value: A, arg: one

=== call b.method("one")
decorate_method_memoize.__get__ is called from <__main__.Sample object at 0x7f2d61cb6350>
decorate_method_memoize.__get__ create wrapper for <__main__.Sample object at 0x7f2d61cb6350>
Decorated by decorate_method_memoize
self.value: B, arg: one

=== call a.method("two")
decorate_method_memoize.__get__ is called from <__main__.Sample object at 0x7f2d61cb7430>
Decorated by decorate_method_memoize
self.value: A, arg: two

ラッパー関数がオブジェクト毎に1つしか作成されていないのがわかる。