[Python] 非同期処理やシングルトンをデコレータで実現する

 今回はPythonの便利なデコレータをいくつか書いていきます。

 デコレータをうまく利用すれば実装の効率化に繋がります。

Pythonのデコレータとは


 Pythonの言語仕様の一つで、関数を修飾する仕組み。

 関数をラッピングする関数というイメージ。関数を引数にとり、元の関数をラッピングした関数を返すイメージ。

 関数にデコレータを指定するとその関数の前後に処理を加えたりができる。クラスにもデコレータを適用できる。

 関数に非同期処理化できるデコレータを指定するだけで非同期処理になったり、使えるようになると非常に便利です。

非同期処理デコレータ


 まずは非同期処理デコレータ。これで一々Thread書く必要がなくなる。

 async_funcがデコレータ。関数を受け取り、その関数をThreadのtargetにすることで別スレッドで実行する仕組み。

 この実装を見ると分かるが、関数のデコレータは関数を受け取り、それをラッピングした関数を返すという内容だ。

from threading import Thread
import time

# 関数を非同期化するデコレータ
def async_func(func):
    def wrapper(*args, **kwargs):
        func_hl = Thread(target=func, args=args, kwargs=kwargs)
        func_hl.start()
        return func_hl

    return wrapper

# 非同期処理デコレータ適用
@async_func
def async_sample():
    time.sleep(0.2)
    print("async func")

async_sample()
print("test")
# 非同期処理化してるため、testの次にasync funcが表示される.

関数の処理時間を測るデコレータ


 次に関数の処理時間を計測するデコレータ。これを使うといちいちprint文なりloggingを挟む必要がなくなる。

 これは単純にtimeを使って関数の前後の時間の差分を測ってprintするのみ。

# 関数の処理時間を測るデコレータ
def measure_calc_time(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print(f"Elapsed time: {time.time() - start}")
        return result

    return wrapper


# 処理を計測
@measure_calc_time
def measure_calc_time_sample():
    print("Start")
    time.sleep(0.5)
    return "End"


print(measure_calc_time_sample())
# 出力は以下の感じになる
# Start
# Elapsed time: 0.500~
# End

クラスをSingletonにするデコレータ


 次にクラスをシングルトン化するデコレータ。

 デコレータはクラスに指定することも可能。その場合はクラス自身を引数にとりクラスをラッピングした関数を返却する実装となる。

 デコレータにインスタンスを保持しておいて、ラッピング関数はインスタンスがあればそれを返し、なければ新たに生成する仕組みだ。

 リストにしないとwrapperから参照できないためこのような実装になっている。

def singleton_class(class_):
    instance = [None, ]

    def wrapper(*args, **kwargs):
        if instance[0] is None:
            instance[0] = class_(*args, **kwargs)
        return instance[0]

    return wrapper


@singleton_class
class SingletonSample:
    def __init__(self):
        self.a = 12


s1 = SingletonSample()
s1.a = 55
s2 = SingletonSample()

# aを変更していて同じインスタンスのため、55, 55と表示される
print(s1.a, s2.a)

補足: デコレータに引数指定したい場合は?


 デコレータに引数を渡すことも可能だ。

 例えば、関数の処理時間を計測するデコレータをdebugモードなら出力、そうでなければ出力しないようにするには以下のようにすればいい。

 今までの引数のないデコレータは、関数をラッピングした関数を返していたが、もう一段ラッピングすることで引数を受け取れるようにする。

import time

# 関数の処理時間を測るデコレータ
def measure_calc_time(debug_mode=True):
    def _measure_calc_time(func):
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            if debug_mode:
                print(f"Elapsed time: {time.time() - start}")
            return result

        return wrapper

    return _measure_calc_time


# 処理を計測
@measure_calc_time(False)
def measure_calc_time_sample():
    print("Start")
    time.sleep(0.5)
    return "End"


print(measure_calc_time_sample())
# debutg_mode=Falseだと出力は以下の感じになる
# Start
# End

コメントを残す

メールアドレスが公開されることはありません。