【Python】デコレータで引数の型チェックを実装する

2023 年 7 月 31 日 by tomokiy

デコレータとは

一番よく目にするデコレータはプロパティのゲッターになるものですかね。

@property
def name(self) -> str:
    return self.__name

このメソッドの上に@propertyのように記述しているものをデコレータと言います。

デコレータの仕組み

以下のようなコードを考えてみます。

def method(func):
    def execute(*args, **kwargs):
        print("処理開始")
        print("引数は、", *args, **kwargs)
        func(*args, **kwargs)
        print("処理終了")
    return execute
​
@method
def test(num:int):
    print(num**2)
​
test(5)

test()を実行すると、まずmethod()が呼び出されます。 method()の引数funcには、test()がfunction型として入っており、 ラップされているexecute()の引数*args, **kwargsには、 デコレータを使用したtest()の引数が全て入ってきます。 なので、func(*args, **kwargs)を実行したタイミングで、 test()の中身が実行されることになります。

これを実行するとこうなります。

$ python test.py
処理開始
引数は、 5
25                                            
処理終了  

引数の型チェックをするデコレータを実装する

import inspect
import functools
​
# ログ文字列
LOG_STR_PARAM_TYPE_ERROR = "引数[{}]の型が異なります。[{}]ではなく[{}]で指定してください。"
​
def method(func):
    """
    ## メソッド用デコレータ
    メソッドに本デコレータを必ず実装する
    """
    def args_type_check(*args, **kwargs):
        """
        ## 引数の型チェック
        引数がアノテーションで指定した型と一致しているかのチェックを行う。
        """
        # メソッドのシグネチャ
        sig = inspect.signature(func)
        # 引数を繰り返す
        for arg_key, arg_val in sig.bind(*args, **kwargs).arguments.items():
            # 引数の本来の型
            annotation = sig.parameters[arg_key].annotation
            # 渡されてきた引数の型
            arg_type = type(arg_val)
            # 引数の型が型クラスでない場合は型チェックを不要にする
            if type(annotation) is type and annotation is not inspect._empty and arg_type is not annotation:
                raise TypeError(LOG_STR_PARAM_TYPE_ERROR.format(arg_key,arg_type,annotation))
        return
​
    @functools.wraps(func)
    def execute_method(*args, **kwargs):
        """
        ## メソッド実行
        メソッドを実行する。
        """
        # 引数の型チェック
        args_type_check(*args, **kwargs)
        # 呼び出し元のメソッドの実行
        result = func(*args, **kwargs)
        return result
    
    return execute_method
​
@method
def test(num:int):
    return num**2
​
print(test(5))

上記をデコレータとして指定することで、引数の型チェックが行えます。

例えば、以下のようにtest()の引数numは整数型なので、5を入れると正常に計算されます。

@method
def test(num:int):
    return num**2
​
print(test(5))
$ python test.py
25

文字列として入れてみると、このようなエラーになります。

@method
def test(num:int):
    return num**2
​
print(test("5"))
$ python test.py
Traceback (most recent call last):
  File "C:\Users\tomokiy\Desktop\test.py", line 48, in <module>
    print(test("5"))
  File "C:\Users\tomokiy\Desktop\test.py", line 37, in execute_method
    args_type_check(*args, **kwargs)
  File "C:\Users\tomokiy\Desktop\test.py", line 27, in args_type_check
    raise TypeError(LOG_STR_PARAM_TYPE_ERROR.format(arg_key,arg_type,annotation))
TypeError: 引数[num]の型が異なります。[<class 'str'>]ではなく[<class 'int'>]で指定してください。

引数の型チェックを行うデコレータの処理の流れ

  1. test()に指定した@methodが処理をキャッチします。
  2. return execute_methodによってdef execute_method(*args, **kwargs):が実行されます。
  3. args_type_check(*args, **kwargs)で引数の型チェックを行います。
  4. sig = inspect.signature(func)で、メソッドのシグネチャというのは、引数やアノテーションなどの情報を持っています。
  5. sig.bind(*args, **kwargs).arguments.items()をfor文で繰り返していますが、ここでそれぞれの引数を順番にチェックしています。
  6. 引数の型がおかしい場合はraise TypeErrorで型エラーにしています。
  7. 引数チェックが正常な場合は、result = func(*args, **kwargs)で元々のメソッドを実行します。
  8. return result戻り値を返してやります。これが無いとtest()の戻り値が機能しません。

★補足★ @functools.wraps(func)をつけることで、デコレータの引数funcから関数名__name__などを取得したときに、元々の関数test()の情報を取得することができます。つけないとexecute_method()の情報になってしまいます。

タグ:

TrackBack