Pythonの関数デコレータ

## 概要
- ラップする関数への呼び出しの前後で追加コードを実行可能
    - 入力の引数や戻り値にアクセスして値を変更したり、例外を送出可能
- デバッグ、関数登録などに役立つ
- 自作でデコレータを定義する場合はfunctoolsのデコレータwrapsを使用
    - 使用しない場合はhelpなどが意図しない振る舞いを行う

## functoolsのデコレータwrapsを未使用
```python:example.py
#!/usr/bin/env python3
# -*- coding: utf_8 -*-
"""関数デコレータ
functoolsのデコレータwrapsを未使用
"""


def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"{func.__name__}({args!r}, {kwargs!r}) -> {result!r}")
        return result

    return wrapper


@trace
def fibonacci(n):
    """フィボナッチ数のn番目の項目を返す

    Parameters
    ----------
    n: int
        項数

    Returns
    -------
    int
        フィボナッチ数
    """
    if n in {0, 1}:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == "__main__":
    fibonacci(5)
    help(fibonacci)
```

```sh
$ python example.py
fibonacci((1,), {}) -> 1
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((3,), {}) -> 2
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((1,), {}) -> 1
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((3,), {}) -> 2
fibonacci((4,), {}) -> 3
fibonacci((5,), {}) -> 5

# help関数の結果
Help on function wrapper in module __main__:

wrapper(*args, **kwargs)
(END)
```

## functoolsのデコレータwraps

```python:example.py
#!/usr/bin/env python3
# -*- coding: utf_8 -*-
"""関数デコレータ
functoolsのデコレータwrapsを使用
"""


from functools import wraps


def trace(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"{func.__name__}({args!r}, {kwargs!r}) -> {result!r}")
        return result

    return wrapper


@trace
def fibonacci(n):
    """フィボナッチ数のn番目の項目を返す

    Parameters
    ----------
    n: int
        項数

    Returns
    -------
    int
        フィボナッチ数
    """
    if n in {0, 1}:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


if __name__ == "__main__":
    fibonacci(5)
    help(fibonacci)
```

```sh
$ python example.py
fibonacci((1,), {}) -> 1
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((3,), {}) -> 2
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((1,), {}) -> 1
fibonacci((0,), {}) -> 0
fibonacci((1,), {}) -> 1
fibonacci((2,), {}) -> 1
fibonacci((3,), {}) -> 2
fibonacci((4,), {}) -> 3
fibonacci((5,), {}) -> 5

# help関数の結果
Help on function fibonacci in module __main__:

fibonacci(n)
    フィボナッチ数のn番目の項目を返す
    
    Parameters
    ----------
    n: int
        項数
    
    Returns
    -------
    int
        フィボナッチ数
(END)
```

## diff
```diff:example.py
--- functoolsのデコレータwrapsを未使用
+++ functoolsのデコレータwraps
@@ -8,10 +8,11 @@
 __version__ = "1.0.1"
 __date__ = "2022/12/08(Created: 2022/12/08)"
 
-
+from functools import wraps
 
 
 def trace(func):
+    @wraps(func)
     def wrapper(*args, **kwargs):
         result = func(*args, **kwargs)
         print(f"{func.__name__}({args!r}, {kwargs!r}) -> {result!r}")
```

## 引数を受け取るデコレータの実装

```python: example.py
#!/usr/bin/env python3
# -*- coding: utf_8 -*-
"""引数が受け取るデコレータ
"""


from collections.abc import Callable
from functools import wraps
from inspect import signature
from typing import Any, ParamSpec

P = ParamSpec("P")


def get_arg_value(func: Callable[P, Any], args: tuple, name: str) -> Any:
    """関数の引数から指定した名前の値を取得

    Args:
        func (Callable[P, Any]): 検索対象の関数
        args (tuple): 取得したい引数の値
        name (str): 取得したい引数名

    Raises:
        Exception: 関数の中に取得したい引数名が存在しない場合

    Returns:
        Any: 引数の値
    """
    sig = signature(func)
    keys = tuple(sig.parameters.keys())
    if name not in keys:
        raise Exception(f"関数の中に引数{name}が存在しません")
    return args[keys.index(name)]


def deco_func(
    app_name: str, id_name: str = "request_id"
) -> Callable[[Callable[P, int]], Callable[P, int]]:
    """デコレータの引数を受け取る部分

    Args:
        app_name (str): 出力したいAPIの引数名
        id_name (str, optional): 出力したいIDの引数名
    """

    def _deco_func(func: Callable[P, int]) -> Callable[P, int]:
        """デコレータの関数を受け取る部分

        Args:
            func (Callable[P, int]): デコレータの関数を受け取る部分

        Raises:
            Exception: 戻り値の型がintと異なる場合

        """

        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> int:
            """関数の前後に処理を差し込む部分

            Raises:
                Exception: 戻り値の型がstrと異なる場合

            Returns:
                int: 関数を実行した結果
            """
            app = get_arg_value(func, args, app_name)
            id = get_arg_value(func, args, id_name)
            print(f"[{id}][START] POST {app} API is called.")
            result = func(*args, **kwargs)
            print(f"[{id}][END] POST {app} API is called.")
            if not isinstance(result, int):
                raise Exception(f"{type(result)}: 戻り値の型が仕様と異なります")
            if result == 200:
                print("[INFO] 成功")
            else:
                print("[ERROR] 失敗")
            return result

        return wrapper

    return _deco_func


@deco_func("application")
def test(db: Any, request_id: Any, application: Any, result: int) -> int:
    """実行例用のスタブ関数"""
    print("関数の処理を実行")
    return result


if __name__ == "__main__":
    test(1, "ID_hoge", "test", 200)

```

```sh
$ python example.py
[ID_hoge][START] POST test API is called.
関数の処理を実行
[ID_hoge][END] POST test API is called.
[INFO] 成功
```

## 参考
- [フォーマット済み文字列リテラル](https://docs.python.org/ja/3.8/tutorial/inputoutput.html#formatted-string-literals)
    - [書式指定ミニ言語仕様](https://docs.python.org/ja/3.8/library/string.html#formatspec)
        -  '!a' は ascii() 
        -  '!s' は str() 
        - '!r' は repr() 
- [inspect --- 活動中のオブジェクトの情報を取得する](https://docs.python.org/ja/3.8/library/inspect.html)