0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python文法100本ノック vol.8 ~デコレ―タ・関数拡張と高階関数~

Posted at

まえがき

Python100本ノックについての記事です。既存の100本ノックは幾分簡単すぎるようにも感じており、それに対するアンサー記事となります。誤りなどがあれば、ご指摘ください。今回は本番編として、デコレータ・関数拡張と高階関数を中心に10問扱います。

Q.75

項目 内容
概要 任意の関数に対して、
引数/戻り値/実行時間を記録し、関数名・例外発生状況を含むログ出力を行うデコレータ @log_execution を設計せよ。
要件
  • 引数:*args, **kwargs をログ出力
  • 戻り値もログに含む
  • 関数名を func.__name__ で取得
  • 関数の実行時間(秒)を time.perf_counter() で計測
発展仕様
  • 例外が発生した場合もログ出力(例外内容・発生時間)
  • ログライブラリは loguru を使用
  • ログレベルを INFO/ERROR/SUCCESS で分類
  • @wraps で関数メタ情報保持
使用構文 @decorator, time.perf_counter, loguru.logger, *args, **kwargs, __name__, traceback, wraps, try-except

A.75

■ 模範解答

import time
import traceback
from functools import wraps
from loguru import logger


def log_execution(func):
    """任意関数の実行をラップし、引数/戻り値/実行時間をログ出力するデコレータ"""
    @wraps(func)  # 元関数の__name__などを保持
    def wrapper(*args, **kwargs):
        start = time.perf_counter()  # 実行開始時刻
        func_name = func.__name__    # ログ用の関数名取得

        logger.info(f"[START] {func_name} args={args}, kwargs={kwargs}")

        try:
            result = func(*args, **kwargs)  # 関数を実行
            elapsed = time.perf_counter() - start  # 実行時間計測

            logger.success(f"[DONE] {func_name} returned={result} in {elapsed:.4f}s")
            return result

        except Exception as e:
            elapsed = time.perf_counter() - start  # 実行時間(失敗時も記録)

            logger.error(f"[ERROR] {func_name} raised {type(e).__name__}: {e} in {elapsed:.4f}s")
            logger.debug(traceback.format_exc())  # スタックトレースも出力(DEBUGログ)

            raise  # 例外は再送出

    return wrapper
実行例1:正常系関数
@log_execution
def add(a, b):
    return a + b

add(3, 5)
実行結果1:出力ログ
[INFO] [START] add args=(3, 5), kwargs={}
[SUCCESS] [DONE] add returned=8 in 0.0001s
実行例2:例外発生系関数
@log_execution
def divide(x, y):
    return x / y

try:
    divide(10, 0)
except ZeroDivisionError:
    pass
実行結果2:出力ログ
[INFO] [START] divide args=(10, 0), kwargs={}
[ERROR] [ERROR] divide raised ZeroDivisionError: division by zero in 0.0001s
[DEBUG] Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

■ 文法・構文まとめ

使用要素 解説
@wraps(func) デコレータ内でも __name__, __doc__ などの情報を元関数から維持する
*args, **kwargs 任意個の引数・キーワード引数を受け取ってロギング対象に
time.perf_counter() 処理時間の計測に高精度なクロックを利用
logger.success() 成功した場合は成功ログとして見やすく出力
logger.debug(traceback.format_exc()) 例外時のスタックトレース出力
try-except 処理失敗でもログに記録後、例外を再送出(アプリケーション全体の制御を妨げない)

Q.76

項目 内容
概要 引数の型に応じて異なる関数を実行する汎用的なディスパッチ機構 @type_dispatch をデコレータとして設計せよ。
高階関数・型判定を活用し、ログと例外対応を含めること。
要件
  • 関数ごとに型→関数マッピングの辞書を保持
  • 最初の引数の型に基づいて関数を選択し実行
  • 一致しない場合は TypeError を発生
  • 関数名・型・戻り値・処理時間をログ出力
発展仕様
  • loguru による INFO / ERROR / SUCCESS ログ
  • @wraps によるメタデータ保持
  • 非呼び出し登録エラー、マルチ登録、汎用関数 fallback 対応
使用構文 dict, type, callable, @wraps, isinstance, try-except, loguru.logger, functools, 高階関数, 関数閉包(クロージャ)

A.76

■ 模範解答

from functools import wraps
from time import perf_counter
from loguru import logger


def type_dispatch():
    """
    引数の型に応じて異なる関数をディスパッチするデコレータファクトリ
    """
    registry = {}       # 型と関数の対応マップ
    default_fn = None   # fallback関数(登録がない場合)

    def register(arg_type):
        def decorator(fn):
            if not callable(fn):
                raise TypeError(f"Handler for {arg_type} must be callable.")
            registry[arg_type] = fn
            logger.info(f"[REGISTER] {arg_type.__name__}{fn.__name__}")
            return fn
        return decorator

    def dispatcher(fn):
        nonlocal default_fn
        default_fn = fn  # 最初の関数を fallback とみなす

        @wraps(fn)
        def wrapper(*args, **kwargs):
            start = perf_counter()
            if not args:
                raise TypeError("[DISPATCH ERROR] No positional argument provided.")

            arg = args[0]
            arg_type = type(arg)

            handler = registry.get(arg_type, default_fn)  # 型が未登録ならfallback

            try:
                result = handler(*args, **kwargs)
                duration = perf_counter() - start
                logger.success(f"[DISPATCH] {handler.__name__}({arg_type.__name__}) → {result} ({duration:.4f}s)")
                return result
            except Exception as e:
                logger.exception(f"[FAILURE] Handler failed for {arg_type.__name__}: {e}")
                raise

        wrapper.register = register  # register機能をアタッチ
        wrapper.registry = registry  # レジストリ参照可能
        return wrapper

    return dispatcher
実行例1:基本的な型分岐
@type_dispatch()
def handle(value):
    raise TypeError(f"No handler for type {type(value)}")

@handle.register(int)
def handle_int(x):
    return x * 2

@handle.register(str)
def handle_str(x):
    return x.upper()


# 使用例
handle(10)       # 20
handle("hello")  # "HELLO"
実行結果1:ログ出力例
[REGISTER] int  handle_int
[REGISTER] str  handle_str
[DISPATCH] handle_int(int)  20 (0.0000s)
[DISPATCH] handle_str(str)  HELLO (0.0000s)
実行例2:例外と未登録型
@type_dispatch()
def safe_handler(x):
    logger.warning(f"Unknown type: {type(x)}")
    return None

@safe_handler.register(float)
def handle_float(x):
    return round(x, 2)

safe_handler(3.14159)   # 3.14
safe_handler([1, 2, 3]) # fallbackにより None
実行結果2:ログ出力例
[DISPATCH] handle_float(float)  3.14 (0.0000s)
[DISPATCH] safe_handler(list)  None (0.0000s)

■ 文法・構文まとめ

要素 解説
@wraps(fn) 関数名・docstring を元関数に保つ
クロージャ+レジストリ registry を保持しつつ、登録&呼び出しを内包関数で構成
arg_type = type(arg) 動的な型判定を利用してマップ検索
wrapper.register 関数属性を動的に付与し、複数登録可能とする
fallback関数 型未登録時にデフォルト関数を呼ぶ(安全設計)
loguru によるログ 成功は SUCCESS、失敗は ERROREXCEPTION、登録は INFO によってレベル分け
perf_counter() 実行時間測定(処理の重さがわかる)

Q.77

項目 内容
概要 関数の引数に対して任意のバリデーション関数(複数)を個別に適用するデコレータ @validate_args を定義せよ。
バリデーション失敗時には ValueError を送出し、ログ出力すること。
要件
  • バリデータは辞書形式 {param_name: validator_func} で指定
  • 関数定義上の引数名と一致する必要あり
  • バリデーション失敗時は即例外+ログ出力
発展仕様
  • 関数引数の名前を inspect.signature から抽出し、動的にバインド
  • loguru によるログ出力(成功・失敗・実行)
  • デフォルト引数対応
使用構文 @decorator, *args, **kwargs, inspect.signature, loguru.logger, raise, 高階関数, callable, zip, wraps, try-except

A.77

■ 模範解答

import inspect
from functools import wraps
from loguru import logger


def validate_args(validators: dict):
    """
    引数ごとのバリデータ関数を辞書で受け取る汎用デコレータ
    """
    def decorator(func):
        sig = inspect.signature(func)  # 引数の署名を取得

        @wraps(func)
        def wrapper(*args, **kwargs):
            bound = sig.bind(*args, **kwargs)  # 引数と値をバインド
            bound.apply_defaults()  # デフォルト値を適用

            for param, validator in validators.items():
                if param in bound.arguments:
                    value = bound.arguments[param]

                    try:
                        if not validator(value):
                            raise ValueError(f"Validation failed: {param} = {value}")
                        logger.info(f"[VALID] {param} = {value} passed")
                    except Exception as e:
                        logger.error(f"[INVALID] {param} = {value}{e}")
                        raise

            logger.info(f"[CALL] {func.__name__} with args={args}, kwargs={kwargs}")
            return func(*args, **kwargs)

        return wrapper
    return decorator
実行例1:数値と文字列のバリデーション
@validate_args({
    "x": lambda x: isinstance(x, int) and x >= 0,
    "name": lambda n: isinstance(n, str) and n.isalpha()
})
def greet(x, name="User"):
    return f"{name} has {x} points."

print(greet(10, "Alice"))   # OK
# print(greet(-5, "Bob"))   # ValueError(xが負)
実行結果1:出力ログ
[VALID] x = 10 passed
[VALID] name = Alice passed
[CALL] greet with args=(10, 'Alice'), kwargs={}
実行例2:デフォルト引数と検出失敗
@validate_args({
    "threshold": lambda t: 0 <= t <= 1
})
def run_model(threshold=0.5):
    return f"Running with threshold={threshold}"

run_model(0.9)      # OK
# run_model(1.5)    # ValueError(不正な閾値)

■ 文法・構文まとめ

要素 解説
inspect.signature() 関数定義の引数名・デフォルト値などを取得
sig.bind(*args, **kwargs) 呼び出し時の実引数を関数のパラメータ名とバインド
bound.apply_defaults() 指定されなかった引数にデフォルト値を補完
validator(value) 各引数に対する条件チェック(True で合格)
@wraps(func) 関数のメタ情報(name, __doc__など)を保つ
loguru.logger バリデーション結果をログ出力。成功:INFO、失敗:ERROR
raise ValueError(...) 失敗時はログ出力後、即例外送出

Q.78

項目 内容
概要 任意の関数に対して、引数と戻り値をキーとした一定秒数の有効期限付きキャッシュ機構を構築せよ。再計算時はキャッシュを自動更新する。ログも記録すること。
要件
  • 同じ引数なら、指定秒数の間はキャッシュを返す
  • 期限切れなら再計算して再キャッシュ
  • ログ出力:ヒット / ミス / 再計算
発展仕様
  • 可変引数 *args, **kwargs に完全対応
  • 例外発生時にはキャッシュをスキップし、ログ出力
  • 引数をハッシュ可能であることを前提とし、キャッシュキーをタプルで構成
使用構文 time.time, wraps, dict, nonlocal, クロージャ, try-except, loguru.logger, hashable check, *args, **kwargs

A.78

■ 模範解答

import time
from functools import wraps
from loguru import logger


def timed_cache(seconds: int = 60):
    """
    一定時間だけ結果をキャッシュするデコレータファクトリ。
    :param seconds: キャッシュの有効期限(秒)
    """
    def decorator(func):
        cache = {}  # (args, frozenset(kwargs.items())) -> (timestamp, value)

        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                key = (args, frozenset(kwargs.items()))  # 引数をキャッシュキーに変換(ハッシュ可能)
            except TypeError:
                logger.warning(f"[CACHE SKIP] 非ハッシュ型引数によりキャッシュ不可: args={args}, kwargs={kwargs}")
                return func(*args, **kwargs)

            now = time.time()
            if key in cache:
                timestamp, value = cache[key]
                if now - timestamp < seconds:
                    logger.success(f"[CACHE HIT] {func.__name__} args={args}, kwargs={kwargs}")
                    return value
                else:
                    logger.info(f"[CACHE EXPIRED] {func.__name__} → 再計算")

            try:
                result = func(*args, **kwargs)
                cache[key] = (now, result)
                logger.info(f"[CACHE SET] {func.__name__} = {result}")
                return result
            except Exception as e:
                logger.exception(f"[CACHE ERROR] {func.__name__}: {e}")
                raise

        return wrapper
    return decorator
実行例1:単純な再計算の有無の確認
@timed_cache(seconds=5)
def slow_add(x, y):
    time.sleep(1)
    return x + y

print(slow_add(1, 2))  # 計算実行
print(slow_add(1, 2))  # キャッシュ命中(速い)
time.sleep(6)
print(slow_add(1, 2))  # キャッシュ失効 → 再計算
実行例2:非ハッシュ型によるキャッシュスキップと例外処理
@timed_cache(seconds=10)
def risky_func(x):
    if isinstance(x, list):  # list は非ハッシュ型
        raise ValueError("リストはダメ")
    return x * 2

# risky_func([1, 2, 3])  # キャッシュされず例外 → ログに記録
print(risky_func(10))      # OK
print(risky_func(10))      # キャッシュヒット

■ 文法・構文まとめ

要素 解説
time.time() 現在のUNIX時間(秒単位)を取得
frozenset(kwargs.items()) kwargs をハッシュ可能にする(順不同対応)
nonlocal cache はクロージャ内で保持され、関数呼び出し間で永続
wraps(func) オリジナル関数のメタ情報を保つ
try-except キャッシュキーの構築や関数本体の例外を検出
loguru.logger 成功・ヒット・失敗・例外を色分け付きで出力可能
dict によるキャッシュ 手動で LRU や TTL を制御できるため、functools.lru_cache より柔軟に振る舞える

Q.79

項目 内容
概要 関数群 f, g, h, ... を順に適用する関数合成器 compose_functions(*funcs) を定義せよ。内部で f(g(h(...(x)))) のように動作するクロージャを返すこと。
要件
  • 左から右への順で関数を適用(Haskell 型の compose)
  • 関数はすべて callable であることを事前チェック
  • 実行時・定義時の例外に対応し、ログを出力
発展仕様
  • loguru による合成ステップのロギング(各関数名と結果)
  • __name__ を持たない関数のハンドリング(lambda など)
  • 合成順序の切り替え引数 reverse=False も設ける
使用構文 lambda, reduce, functools.reduce, callable, *args, loguru.logger, wraps, クロージャ、例外処理構文(try-except

A.79

■ 模範解答

from functools import reduce
from loguru import logger

def compose_functions(*funcs, reverse=False):
    """
    複数の関数を順に適用して合成関数を返す。
    :param funcs: 合成したい関数群
    :param reverse: 適用順を反転(Trueなら右から左、Falseなら左から右)
    :return: 合成関数
    """
    # 関数の妥当性検証
    for f in funcs:
        if not callable(f):
            raise TypeError(f"非callableオブジェクトが含まれています: {f}")

    # 適用順の制御
    chain = funcs[::-1] if reverse else funcs

    def composed(x):
        result = x
        for func in chain:
            try:
                fname = getattr(func, "__name__", repr(func))  # 名前またはrepr
                logger.info(f"[APPLY] {fname}({result})")
                result = func(result)
                logger.success(f"[RESULT] → {result}")
            except Exception as e:
                logger.exception(f"[ERROR] 関数 {fname} の適用中に例外発生")
                raise
        return result

    return composed
実行例1:通常の合成適用
def double(x): return x * 2
def square(x): return x ** 2
def negate(x): return -x

composed = compose_functions(double, square, negate)
print(composed(3))  # → double(square(negate(3))) = double(9) = 18
実行結果1:出力ログ
[APPLY] negate(3)
[RESULT]  -3
[APPLY] square(-3)
[RESULT]  9
[APPLY] double(9)
[RESULT]  18
実行例2:lambda混在+順序逆転(reverse=True)
f = compose_functions(
    lambda x: x + 1,
    lambda x: x * 10,
    lambda x: x - 5,
    reverse=True
)
print(f(2))  # → ((2 - 5) * 10) + 1 = -29

■ 文法・構文まとめ

文法要素 解説
reduce() 本来関数合成にも使用可能だが、逐次ログ出力のために for ループで代用
callable(f) 各要素が呼び出し可能(関数やlambda等)かをチェック
func.__name__ ロギング時の関数名を取得(匿名関数の場合は repr(func) で代用)
reverse 引数 Trueで右から左(数学的合成: f∘g∘h)/Falseで左から右(パイプライク: h → g → f)の適用順を指定
クロージャ 合成関数内部で chain を閉じ込め、入力 x に対して順に適用
loguru.logger 各ステップの適用ログ・例外ログを出力
try-except 各関数呼び出し時に例外が発生しても適切に報告・中断

Q.80

項目 内容
概要 任意の関数に対して、一度だけ実行し、以降はキャッシュ済みの結果を返すデコレータ @once を実装せよ。関数は引数に関係なく一度しか実行されず、結果は固定される。
要件
  • 関数は一度だけ評価
  • 複数回の呼び出しでは同じ結果を返す(引数無視)
  • ロギングで「初回実行/キャッシュ返却」を明示
  • 例外時はキャッシュせず再実行を許可
発展仕様
  • 状態を保持するクロージャ
  • try-except による初回実行の例外補足
  • 結果・例外ログを loguru で記録
  • 関数メタ情報保持 (@wraps)
使用構文 nonlocal, *args, **kwargs, wraps, try-except, loguru.logger, クロージャ, 初期化フラグとキャッシュ変数

A.80

■ 模範解答

from functools import wraps
from loguru import logger


def once(func):
    """
    関数を一度だけ実行し、以降はその結果をキャッシュして返すデコレータ。
    引数は無視され、常に同じ結果を返す。
    """
    executed = False       # 実行済みフラグ
    result = None          # 実行結果を保持

    @wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal executed, result  # クロージャ内で変更可能にする
        if executed:
            logger.debug(f"[ONCE] Cached result returned: {func.__name__} -> {result}")
            return result
        try:
            logger.info(f"[ONCE] First execution of: {func.__name__}")
            result = func(*args, **kwargs)  # 初回実行
            executed = True
            logger.success(f"[ONCE] Execution result cached: {result}")
            return result
        except Exception as e:
            logger.exception(f"[ONCE] First execution failed: {e}")
            raise

    return wrapper
実行例1:通常の初回実行と以降のキャッシュ返却
@once
def init_config():
    print("Config initialized")
    return {"version": 1.2, "mode": "debug"}

print(init_config())  # 初回 → 実行される
print(init_config())  # 以降 → キャッシュが返る
実行結果1:出力ログ
Config initialized
{'version': 1.2, 'mode': 'debug'}
{'version': 1.2, 'mode': 'debug'}
実行例2:初回で例外 → キャッシュされず、再試行される
counter = {"calls": 0}

@once
def flaky():
    counter["calls"] += 1
    if counter["calls"] < 2:
        raise RuntimeError("一時的な失敗")
    return "成功!"

try:
    flaky()
except RuntimeError:
    print("失敗したが次で成功するはず")

print(flaky())  # 2回目は成功 → キャッシュされる
print(flaky())  # 以降はキャッシュ結果

■ 文法・構文まとめ

文法要素 解説
nonlocal クロージャ変数 executed, resultwrapper 関数から書き換えるために必要
@wraps(func) オリジナル関数の __name__, __doc__ などのメタ情報を保持
loguru.logger 各ステップ(初回実行・キャッシュ返却・例外)の記録
try-except 初回実行で例外が起きた場合はキャッシュせず、再実行可能とする安全設計
引数の無視 引数の有無に関係なく1回しか関数を評価しないため、関数の性質として「一度しか意味がない」場面(初期化・設定など)に適する

Q.81

項目 内容
概要 任意の関数に適用可能な @retry(n=3, exceptions=(Exception,)) デコレータを定義せよ。指定された例外が発生した場合に最大 n 回まで再実行する。
要件
  • 最大 n 回までリトライ可能(成功したら即 return)
  • 例外型をタプルで指定可能
  • loguru によるリトライ試行ログ
  • wraps による関数保持
発展仕様
  • リトライ間の待機時間を設定可能(delay=0
  • 成功時のログ・失敗時のログを出力
  • リトライ回数と例外発生詳細を明記
使用構文 for, try-except, break, wraps, *args, **kwargs, nonlocal, デフォルト引数付きデコレータ, loguru.logger, time.sleep

A.81

■ 模範解答

import time
from functools import wraps
from loguru import logger


def retry(n=3, exceptions=(Exception,), delay=0):
    """
    指定された例外が発生した場合、最大 n 回までリトライを行うデコレータ。
    
    :param n: 最大試行回数
    :param exceptions: 対象となる例外タプル
    :param delay: 再試行までの待機秒数
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, n + 1):  # 試行は 1 から n まで
                try:
                    logger.info(f"[RETRY] {func.__name__} 試行 {attempt} 回目")
                    result = func(*args, **kwargs)
                    logger.success(f"[RETRY] 成功: {func.__name__}{result}")
                    return result
                except exceptions as e:
                    logger.warning(f"[RETRY] 例外発生({type(e).__name__}): {e}")
                    last_exception = e
                    if attempt < n:
                        logger.debug(f"[RETRY] {delay} 秒待機して再試行")
                        time.sleep(delay)
            logger.error(f"[RETRY] 全 {n} 回失敗: {func.__name__}")
            raise last_exception
        return wrapper
    return decorator
実行例1:2回目で成功する関数
counter = {"failures": 0}

@retry(n=3, exceptions=(ValueError,), delay=1)
def sometimes_fails():
    counter["failures"] += 1
    if counter["failures"] < 2:
        raise ValueError("一時的な失敗")
    return "成功"

print(sometimes_fails())  # 2回目で成功 → "成功"
実行例2:常に失敗する関数
@retry(n=2, exceptions=(RuntimeError,))
def always_fails():
    raise RuntimeError("毎回失敗するよ")

try:
    always_fails()
except RuntimeError as e:
    print(f"最終的に失敗: {e}")

■ 文法・構文まとめ

文法要素 説明
@retry(n=3) デフォルト引数付きのデコレータ(デコレータ工場関数)
wraps(func) 関数の __name__, __doc__ などの情報をデコレータが破壊しないように保持
for attempt in リトライ試行を制御
try-except 対象例外だけを捕捉し、それ以外の例外はスロー
delay time.sleep() による再試行のインターバル
loguru.logger 成功・警告・エラーのロギング制御(試行回数や例外名、結果などの出力を含む)
last_exception 最後の例外を保存してリトライ上限に達した後に明示的に再送出(raise)

Q.82

項目 内容
概要 任意関数に適用できるデコレータ @with_context_log を設計せよ。UUID を使って実行 ID を生成し、関数実行の「開始」「終了」「失敗」などの文脈を loguru で記録せよ。
要件
  • 各関数呼び出しごとに一意の context_iduuid.uuid4() で生成
  • 実行前:開始ログ、実行後:正常終了ログ、例外時:例外ログを記録
  • ログには関数名・引数情報・時刻を含める
発展仕様
  • 戻り値もログに含める
  • ログ出力フォーマットの整備(時刻付き・読みやすさ配慮)
  • 関数識別子(モジュール+関数名)もログに含める
使用構文 uuid.uuid4, datetime.now, wraps, loguru.logger, *args, **kwargs, クロージャ, 例外処理 (try-except)

A.82

■ 模範解答

import uuid
from datetime import datetime
from functools import wraps
from loguru import logger


def with_context_log(func):
    """
    関数の実行開始・終了・失敗を一意の context_id と共に loguru に記録するデコレータ。
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        context_id = str(uuid.uuid4())  # 各呼び出しに一意な ID を付与
        func_name = f"{func.__module__}.{func.__qualname__}"  # モジュール+関数名
        start_time = datetime.now()
        logger.info(f"[{context_id}] START: {func_name} at {start_time} with args={args}, kwargs={kwargs}")
        try:
            result = func(*args, **kwargs)  # 関数本体の実行
            end_time = datetime.now()
            duration = (end_time - start_time).total_seconds()
            logger.success(f"[{context_id}] END: {func_name} at {end_time} (duration={duration:.3f}s) → result={result!r}")
            return result
        except Exception as e:
            logger.exception(f"[{context_id}] ERROR in {func_name}: {e}")
            raise
    return wrapper
実行例1:正常実行のケース
@with_context_log
def add(a, b):
    return a + b

add(3, 5)  # → START → END ログ出力
実行例2:例外発生のケース
@with_context_log
def divide(x, y):
    return x / y

try:
    divide(10, 0)  # → ZeroDivisionError ログと例外出力
except ZeroDivisionError:
    pass
実行結果2:出力ログ(整形済み)
2025-08-02 13:45:00.100 | INFO     | [a1b2c3...] START: __main__.add at 2025-08-02 13:45:00.100 with args=(3, 5), kwargs={}
2025-08-02 13:45:00.102 | SUCCESS  | [a1b2c3...] END: __main__.add at 2025-08-02 13:45:00.102 (duration=0.002s)  result=8

■ 文法・構文まとめ

要素 解説
uuid.uuid4() 各関数呼び出しに一意な ID を生成し、ログトレースを容易にするための手法
@wraps(func) デコレータで元関数の __name____doc__ を保持
loguru.logger INFO, SUCCESS, ERROR ログをレベル別に明確化
*args, **kwargs すべての関数引数を受け取り、ログとして記録
try-except + logger.exception() 例外が起きた際にも traceback をログに含める
datetime.now() 実行時刻と処理時間の記録
func.__qualname__ 関数の完全修飾名を取得(メソッドにも対応)

Q.83

項目 内容
概要 任意の既存デコレータを条件付きで適用できる @maybe(decorator, enable=True) を実装せよ。
enable=False の場合は元関数を素通しで返すこと。ログ記録やエラーハンドリングも含めること。
要件
  • 引数に別のデコレータを取り、enable に応じてそのデコレータを適用/無視できる高階・高階関数
  • loguru による有効/無効ログ記録
  • wraps によるメタデータ保持
発展仕様
  • デコレータが関数ではなく None だった場合のバリデーション
  • ログに関数名と有効/無効状態を含める
  • 他のデコレータ(@with_context_log 等)と組み合わせ可能にする
使用構文 wraps, callable, *args, **kwargs, 高階関数(関数を返す関数を返す構造), loguru.logger

A.83

■ 模範解答

from functools import wraps
from loguru import logger


def maybe(decorator, enable=True):
    """
    指定された decorator を有効・無効に切り替えられる高階高階関数。
    """
    if decorator is not None and not callable(decorator):
        raise ValueError("Decorator must be callable or None")

    def outer(func):
        if not callable(func):
            raise TypeError("Target must be callable")

        if not enable:
            @wraps(func)
            def identity(*args, **kwargs):
                logger.info(f"[maybe] Skipping decorator for function '{func.__name__}' (disabled).")
                return func(*args, **kwargs)
            return identity

        logger.info(f"[maybe] Applying decorator to function '{func.__name__}' (enabled).")
        return decorator(func)

    return outer
利用例
import time
from loguru import logger

def timing(func):
    """処理時間をログ出力するデコレータ"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        logger.success(f"[timing] {func.__name__} completed in {end - start:.6f}s.")
        return result
    return wrapper
実行例1:有効な場合
@maybe(timing, enable=True)
def slow_add(a, b):
    time.sleep(0.2)
    return a + b

print(slow_add(3, 4))
実行結果1:出力ログ
[maybe] Applying decorator to function 'slow_add' (enabled).
[timing] slow_add completed in 0.201245s.
7
実行例2:無効な場合
@maybe(timing, enable=False)
def fast_add(a, b):
    time.sleep(0.1)
    return a + b

print(fast_add(10, 5))
実行結果2:出力ログ
[maybe] Skipping decorator for function 'fast_add' (disabled).
15

■ 文法・構文まとめ

項目 説明
wraps(func) 元の関数名・docstring を保つための標準的デコレータ
callable(decorator) 引数が関数であることを検証。None や誤った型に対する堅牢性強化
logger.info / success 条件ごとの分岐ログを出力し、何が適用されたかを追いやすく
高階高階関数構造 maybe(decorator, enable)(func) の3階層構造(デコレータを受け取る・条件で分岐・関数を返す)
identity関数(素通し) enable=False 時に元関数を何も加工せず返す。これにより柔軟なスイッチ可能なデコレータ構築が可能
@maybe(...)の応用 テスト時にデコレータだけオフにする、動的条件で切り替えるなどの柔軟なユースケースに活用可能

Q.84

項目 内容
概要 任意のログレベル・メッセージフォーマット・ロガー名を指定可能な動的ログ生成用デコレータファクトリ関数を実装せよ。関数名・引数・戻り値の埋め込みにも対応すること。
要件
  • loguru を使用してログ出力
  • ログレベル(info, warning 等)を文字列で指定
  • 動的メッセージフォーマット対応({func_name} など)
  • ロガー名付与可
発展仕様
  • 関数失敗時には自動で logger.exception を発動
  • ログ出力前にパラメータ型チェック
  • ログ出力をON/OFF切り替え可能
使用構文 @decorator_factory, wraps, *args, **kwargs, f-string, str.format_map, try-except, loguru.logger.bind()、メタ情報動的挿入

A.83

■ 模範解答

from functools import wraps
from loguru import logger
import inspect


def dynamic_logger(level="info", fmt="{func_name} called with {args}", logger_name=None, enable=True):
    """
    ログレベル・メッセージ・ロガー名を動的に指定できるデコレータファクトリ。
    """
    if not isinstance(fmt, str) or not isinstance(level, str):
        raise ValueError("`fmt` and `level` must be strings")

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # ロガーの構築(bind を使って名前空間を分離)
            log = logger if logger_name is None else logger.bind(name=logger_name)

            # 関数メタ情報の準備
            func_name = func.__name__
            bound_args = inspect.signature(func).bind(*args, **kwargs)
            bound_args.apply_defaults()
            call_args = dict(bound_args.arguments)

            try:
                result = func(*args, **kwargs)

                if enable:
                    # ログ出力(成功時)
                    message = fmt.format_map({
                        "func_name": func_name,
                        "args": call_args,
                        "result": result
                    })
                    log_method = getattr(log, level, log.info)
                    log_method(message)

                return result

            except Exception as e:
                if enable:
                    log.exception(f"{func_name} raised an exception: {e}")
                raise

        return wrapper
    return decorator
実行例1:info ログを関数呼び出しに出力
@dynamic_logger(level="info", fmt="{func_name}({args}) => {result}", logger_name="math_logger")
def multiply(x, y):
    return x * y

multiply(3, 7)
実行結果1:出力ログ
math_logger: multiply({'x': 3, 'y': 7}) => 21
実行例2:エラー時ログ出力+例外送出
@dynamic_logger(level="warning", fmt="{func_name}({args}) failed", enable=True)
def risky_div(a, b):
    return a / b  # ゼロ除算例外が起こりうる

risky_div(10, 0)
実行結果2:出力ログ
risky_div({'a': 10, 'b': 0}) failed
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero

■ 文法・構文まとめ

項目 解説
@decorator_factory構造 デコレータを返す関数を返す構造。@dynamic_logger(...) のように使うには最低2段階の関数が必要。
wraps(func) デコレートした関数の __name__ や docstring を元の関数と一致させるための装飾。
loguru.logger.bind() 名前付きロガーを作る。ログ出力の分類・トラッキングがしやすくなる。
inspect.signature().bind() 実行時に *args**kwargs を引数名付きで解釈することで、ログに {x=..., y=...} のような表示を実現。
str.format_map() キーが不足しても KeyError を出さないフォーマット処理。カスタムログメッセージに使いやすい。
getattr(log, level) level 文字列をメソッド(info, debug, warning)に変換。存在しない場合に備えてデフォルト log.info を与えてある。
logger.exception(...) try-except ブロックで発生した例外をログ出力しつつ再送出。実行時トレースログを残すための Pythonic 例外ロギング。
0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?