見出し画像

デコレータを使ってログの処理や例外処理 Python

前置き

デコレータを使って最初みたとき、活用場所ってそんなにあるのかな?って思っていた。長らくプログラマとして仕事をしている方々からすると「いや、あるだろ」ってツッコミがありそうですが、趣味で開発をしていた自分としてはなかなか使う場面がなかった。

ですが、実務となるとログの処理や例外処理に関して積極的にコードを書く機会が発生し、複数の関数に同じようなログ処理や例外処理を適用するときにデコレータが便利だった。

本題

まずおさらいとしてのデコレータ

def come_home() -> None:
    print("まずは手洗い!")
    print("ただいまー")


def cook(food: str) -> None:
    print("まずは手洗い!")
    print(f"{food}を作ります")


def eat(food: str) -> None:
    print("まずは手洗い!")
    print(f"{food}を食べます")


# 使ってみる
come_home()
cook("カレー")
eat("カレー")

# 結果
まずは手洗い!
ただいまー
まずは手洗い!
カレーを作ります
まずは手洗い!
カレーを食べます

って感じで複数のメソッドで、共通の処理があった場合にデコレータを使用すると以下のように使用すると簡素化できる

from typing import Callable
from functools import wraps


# 手洗いの関数
def wash_hands(func: Callable) -> Callable:
    @wraps(func)
    def wrapper(*args, **kwargs):
        """かならず手洗いをしよう"""
        print("まずは手洗い!")
        func(*args, **kwargs)

    return wrapper


@wash_hands
def come_home() -> None:
    print("ただいまー")


@wash_hands
def cook(food: str) -> None:
    print(f"{food}を作ります")


@wash_hands
def eat(food: str) -> None:
    print(f"{food}を食べます")


# 使ってみる
come_home()
cook("カレー")
eat("カレー")


# 結果
まずは手洗い!
ただいまー
まずは手洗い!
カレーを作ります
まずは手洗い!
カレーを食べます

ってデコレータが便利なのはわかるんですが、趣味で開発していて、あまりこういう場面ないなぁって思ったんですが、冒頭でも話た通り、やはり仕事となると趣味の開発では手抜きをしてきた例外処理やログの処理の実装が不可欠になってきて、使う機会がめちゃくちゃ増えました。

import logging
import sys

logging.basicConfig(level=logging.INFO, stream=sys.stdout)


def input_num_1() -> int:
    """ユーザーから数値を入力する関数その1"""
    try:
        input_num = int(input("Enter a number: "))
        logging.info(f"input_num: {input_num}")

        # 1個目の入力値に対する何か処理

        return input_num
    except ValueError as e:
        print("Please enter a number.")
        logging.error(e)


def input_num_2() -> int:
    """ユーザーから数値を入力する関数その2"""
    try:
        input_num = int(input("Enter a number: "))
        logging.info(f"input_num: {input_num}")

        # 2個目の入力値に対する何か処理

        return input_num
    except ValueError as e:
        print("Please enter a number.")
        logging.error(e)



if __name__ == "__main__":
    input_num_1()
    input_num_2()

って感じで、関数を作っていたら、意識していなかったが、同じ例外処理やログの出力を入れているなぁと思って、共通化できないか検討したところ今回のデコレータが役立ちました。

from typing import Callable
from functools import wraps
import logging
import sys

logging.basicConfig(level=logging.INFO, stream=sys.stdout)

def input_num_exception_handler(func: Callable) -> Callable:
    @wraps(func)
    def wrapper() -> int | None:
        """例外処理を行うデコレータ"""
        try:
            user_input = func()
            logging.info(f"User input: {user_input}")
            return user_input
        except ValueError as e:
            print("Please enter a number.")
            logging.error(f"ValueError: {e}")

    return wrapper


@input_num_exception_handler
def input_num_1() -> int:
    """ユーザーから数値を入力する関数その1"""
    input_num = int(input("Enter a number: "))
    return input_num


@input_num_exception_handler
def input_num_2() -> int:
    """ユーザーから数値を入力する関数その2"""
    input_num = int(input("Enter a number: "))
    return input_num


if __name__ == "__main__":
    input_num_1()
    input_num_2()

とてもスッキリ!
例外処理が分離できて、正常な処理のフローも追いやすくなった。

ログの処理と例外処理のデコレータを分ける

デコレータをチェーンのように複数適用することで、ログ記録 → 例外処理 → 実行といった順序で処理を流すこともできる。

@log_decorator
@exception_handler
def some_function():
    print("関数の実行中")

引数付きのデコレータにして、同じデコレータをしている部分でも柔軟にカスタマイズしても良いなと思った

from typing import Callable
from functools import wraps
import logging
import sys

logging.basicConfig(level=logging.INFO, stream=sys.stdout)


def input_num_exception_handler(msg: str) -> Callable:
    def decorator(func: Callable) -> Callable:
        """例外処理を行うデコレータを返す関数"""
        @wraps(func)
        def wrapper() -> int | None:
            """例外処理を行うデコレータ"""
            try:
                user_input = func()
                logging.info(f"User input: {user_input}")
                return user_input
            except ValueError as e:
                print(msg)
                logging.error(f"ValueError: {e}")
        return wrapper
    return decorator


@input_num_exception_handler(msg="Please enter a number.")
def input_num_1() -> int:
    """ユーザーから数値を入力する関数その1"""
    input_num = int(input("Enter a number: "))
    return input_num


@input_num_exception_handler(msg="おいおい、数字を入れろって言ってるだろ?脳無しか?")
def input_num_2() -> int:
    """ユーザーから数値を入力する関数その2"""
    input_num = int(input("Enter a number: "))
    return input_num
Enter a number: a
Please enter a number.
ERROR:root:ValueError: invalid literal for int() with base 10: 'a'
Enter a number: a
おいおい、数字を入れろって言ってるだろ?脳無しか?
ERROR:root:ValueError: invalid literal for int() with base 10: 'a'

関連記事:


いいなと思ったら応援しよう!