0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonの関数デコレータ

Posted at

概要

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

functoolsのデコレータwrapsを未使用

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)
$ 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

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)
$ 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

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}")

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

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)

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

参考

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?