通常のデコレータはインスタンスメソッドを想定していますが、クラスメソッドやスタティックメソッド、さらには関数としての呼び出しにも対応させたい場合があります。
問題の背景
単純なデコレータはメソッドに適用すると self を正しく処理しますが、クラスメソッドやスタティックメソッドとの組み合わせで問題が起きることがあります。
from functools import wraps def logger(func): @wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__}") return func(*args, **kwargs) return wrapper class MyClass: @logger @classmethod def class_method(cls): pass # 順序によってはエラー
ディスクリプタを使った解決策
デコレータ自体をディスクリプタにすることで、どのような呼び出し方にも対応できます。
from functools import wraps class UniversalDecorator: def __init__(self, func): self.func = func wraps(func)(self) def __call__(self, *args, **kwargs): print(f"Calling {self.func.__name__}") return self.func(*args, **kwargs) def __get__(self, obj, objtype=None): if obj is None: return self return BoundMethod(self, obj) class BoundMethod: def __init__(self, decorator, obj): self.decorator = decorator self.obj = obj def __call__(self, *args, **kwargs): print(f"Calling {self.decorator.func.__name__}") return self.decorator.func(self.obj, *args, **kwargs)
より簡潔な実装
types.MethodType を活用する方法もあります。
import types from functools import wraps class MethodDecorator: def __init__(self, func): self.func = func wraps(func)(self) def __call__(self, *args, **kwargs): print(f"Calling {self.func.__name__}") return self.func(*args, **kwargs) def __get__(self, obj, objtype=None): if obj is None: return self return types.MethodType(self, obj) class Example: @MethodDecorator def method(self): return "method"
関数とメソッド両方に対応
@MethodDecorator def standalone(): return "standalone" e = Example() print(e.method()) # メソッドとして動作 print(standalone()) # 関数として動作
この手法は、ロギング、認証、キャッシュなど汎用的なデコレータを作る際に有用です。