python デコレーター

python Python

デコレーターはよく知られたオブジェクト指向プログラミングのデザインパターンです。オブジェクトの機能や振る舞いを動的に拡張するのですが、その際に継承を使わず元のオブジェクトをラッピングしてシンプルな拡張を実現します。Pythonでは言語レベルでこれに対応しています。元のデザインパターンのデコレーターは主としてC++などでクラスの振る舞いを拡張する手法でしたが、Pythonでは関数オブジェクトを拡張する形がよく利用されているようです。

最小の構成

まず最もシンプルな使用例をあげます。

def show_name(func):
    def wrapper():
        print(f'__name__: {func.__name__}')
        return func()
    return wrapper

@show_name  #show_nameでラッピング(デコレート)
def say():
    print("hello")

say()

# __name__: say
# hello

say()関数をshow_name関数で拡張しています。実は次のコードと振る舞いは同じです。つまり、デコレーターはクロージャーの機能を利用しています。

def show_name(func):
    def wrapper():
        print(f'__name__: {func.__name__}')
        return func()
    return wrapper

def say():
    print("hello")

show_name(say)()

少し現実的なサンプル(引数をとる関数)

少し現実的な例を見てみます。

def simple_timer(func):
    def wrapper(*args, **kwargs):
        import time, functools
        start = time.perf_counter()
        result = func(*args, **kwargs)   # ターゲットの関数を実行
        dur = time.perf_counter() - start
        print(f"[{func.__name__}] 実行時間: {dur:.3f} 秒")
        return result
    return wrapper

@simple_timer    
def f_heavy(n):
    return sum(range(n))

f_heavy(5_000_000)

元の関数をタイマー機能でラップして、実行時間を計測する機能を拡張しました。時間のかかる処理を実行する関数でその実行時間を計測するという例です。関数本体の実行の前後でタイマーを使って計測を行います。

wraps

デコレーターを使うときには@wrapsがよく利用されます。

from functools import wraps

def decorator_(func):
    #@wraps(func)
    def wrapper(*args, **kwargs):
        """ラッパー"""
        result = func(*args, **kwargs)
        return result
    return wrapper

@decorator_
def my_func(x, y):
    """関数本体"""
    return x + y

print(f"__name__ {my_func.__name__}")  # 出力: my_function
print(f"__doc__ {my_func.__doc__}")  # 出力: This is my function.

my_func(1, 2)

このサンプルはそのまま実行すると
__name__ wrapper
__doc__ ラッパー

を返しますが、@wraps(func)をコメントアウトすると

__name__ my_func
__doc__ 関数本体

と変わります。

@wraps(func)がないと、my_funcの属性はラッパーによって上書きされてしまい柾うが、@wrapによってmy_func自体の属性が維持されます。テストの実行時やドキュメントの作成にはかかせないポイントとなります。

クラスへ適用するデコレーター

先にも述べた通り、デコレーターパターンは元々クラスを拡張するアイデアでした。Pythonでもクラス(オブジェクト)にデコレーターを適用することができます。

def class_decorator(cls):
    class WrappedClass(cls):
        def new_method(self):
            return "拡張したメソッド"
    return WrappedClass

@class_decorator
class MyClass:
    def original_method(self):
        return "元々のメソッド"

obj = MyClass()
print(obj.original_method())  # "元々のメソッド"
print(obj.new_method())       # "拡張したメソッド"

この例ではクラスMyClassに新たなメソッドを追加しています。

from datetime import datetime
import time

def modify_instance(cls):
    class Wrapper(cls):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.extra_attribute = f"{self.name}に{datetime.now()}追加した属性"
    return Wrapper

@modify_instance
class MyClass:
    def __init__(self, name):
        self.name = name

obj = MyClass("元のクラス")
print(obj.name)
print(obj.extra_attribute)
time.sleep(2)
obj = MyClass("元のクラス")
print(obj.name)
print(obj.extra_attribute)

# 元のクラス
# 元のクラスに追加した属性

この例は、クラスのインスタンスに拡張属性を追加しました。extra_attributeは本来インスタンスごとに設定できる属性ですが、ここでは拡張したクラスのインスタンスのextra_attributeを強制的に設定しています。

コメント

タイトルとURLをコピーしました