まえがき
Python100本ノックについての記事です。既存の100本ノックは幾分簡単すぎるようにも感じており、それに対するアンサー記事となります。誤りなどがあれば、ご指摘ください。今回は本番編として、デコレータ・関数拡張と高階関数を中心に10問扱います。
Q.75
項目 | 内容 |
---|---|
概要 | 任意の関数に対して、 引数/戻り値/実行時間を記録し、関数名・例外発生状況を含むログ出力を行うデコレータ @log_execution を設計せよ。
|
要件 |
|
発展仕様 |
|
使用構文 |
@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 をデコレータとして設計せよ。高階関数・型判定を活用し、ログと例外対応を含めること。 |
要件 |
|
発展仕様 |
|
使用構文 |
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 、失敗は ERROR /EXCEPTION 、登録は INFO によってレベル分け |
perf_counter() |
実行時間測定(処理の重さがわかる) |
Q.77
項目 | 内容 |
---|---|
概要 | 関数の引数に対して任意のバリデーション関数(複数)を個別に適用するデコレータ @validate_args を定義せよ。バリデーション失敗時には ValueError を送出し、ログ出力すること。 |
要件 |
|
発展仕様 |
|
使用構文 |
@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
項目 | 内容 |
---|---|
概要 | 任意の関数に対して、引数と戻り値をキーとした一定秒数の有効期限付きキャッシュ機構を構築せよ。再計算時はキャッシュを自動更新する。ログも記録すること。 |
要件 |
|
発展仕様 |
|
使用構文 |
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)))) のように動作するクロージャを返すこと。 |
要件 |
|
発展仕様 |
|
使用構文 |
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 を実装せよ。関数は引数に関係なく一度しか実行されず、結果は固定される。 |
要件 |
|
発展仕様 |
|
使用構文 |
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 , result を wrapper 関数から書き換えるために必要 |
@wraps(func) |
オリジナル関数の __name__ , __doc__ などのメタ情報を保持 |
loguru.logger |
各ステップ(初回実行・キャッシュ返却・例外)の記録 |
try-except |
初回実行で例外が起きた場合はキャッシュせず、再実行可能とする安全設計 |
引数の無視 | 引数の有無に関係なく1回しか関数を評価しないため、関数の性質として「一度しか意味がない」場面(初期化・設定など)に適する |
Q.81
項目 | 内容 |
---|---|
概要 | 任意の関数に適用可能な @retry(n=3, exceptions=(Exception,)) デコレータを定義せよ。指定された例外が発生した場合に最大 n 回まで再実行する。 |
要件 |
|
発展仕様 |
|
使用構文 |
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 で記録せよ。 |
要件 |
|
発展仕様 |
|
使用構文 |
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 の場合は元関数を素通しで返すこと。ログ記録やエラーハンドリングも含めること。 |
要件 |
|
発展仕様 |
|
使用構文 |
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
項目 | 内容 |
---|---|
概要 | 任意のログレベル・メッセージフォーマット・ロガー名を指定可能な動的ログ生成用デコレータファクトリ関数を実装せよ。関数名・引数・戻り値の埋め込みにも対応すること。 |
要件 |
|
発展仕様 |
|
使用構文 |
@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 例外ロギング。 |