デコレータ設計のレビュー観点:副作用の特定と責務の分離
デコレータ設計のレビュー観点:副作用の特定と責務の分離
Pythonのデコレータ構文は、関数やクラスに共通処理を付加するための強力なメカニズムである。
しかし、レビュー現場では装飾対象の責務が不明瞭になる、副作用がコード外に潜む、テストが困難になるといった問題がしばしば発生する。
本マニュアルでは、レビューアーがデコレータをどう読み解き、副作用の有無・責務の境界・可読性・再利用性の観点から、安全性と明瞭性の高いコード設計を支援するためのレビュー観点を整理する。
デコレータの基本構造とレビューの対象範囲
基本的なデコレータ
def logger(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@logger
def run():
...このようなデコレータは一見明確だが、レビューで確認すべきは以下の点である。
| チェックポイント | 内容 |
|---|---|
| デコレータの副作用 | printやログ、状態変化などが内包されていないか |
| 引数・戻り値の変化 | オリジナルの関数シグネチャが保持されているか |
| 呼び出し文脈への依存 | 呼び出し側の構造に依存していないか |
| テスト可能性 | モック可能な構造になっているか |
functools.wrapsとは
functools.wraps はデコレータの内側で使用し、元関数の__name__, __doc__, __annotations__などの属性を維持するための関数。
これを使用しないと、関数のメタデータが失われ、デバッグ・静的解析・ドキュメント出力などで不具合が発生する。
見逃されがちな副作用の例
パターン1:外部I/Oへの書き出し
ログ出力の副作用
def audit(func):
def wrapper(*args, **kwargs):
with open("log.txt", "a") as f:
f.write(f"Call: {func.__name__}\n")
return func(*args, **kwargs)
return wrapper副作用レビュー
@Reviewer: `audit`デコレータ内でファイルI/Oが発生しています。テストやリトライ制御時に意図しないログが残るため、明示的な副作用設計が必要です。責務の曖昧なデコレータ設計
パターン2:認可・ログ・状態更新が混在
責務混在の例
def secure(func):
def wrapper(*args, **kwargs):
if not current_user.is_admin():
raise PermissionError
log_access(func.__name__)
update_user_state()
return func(*args, **kwargs)
return wrapper責務レビュー
@Reviewer: 単一デコレータに「認可」「ログ」「状態操作」の3つの責務が集約されています。関心事を分離し、複数デコレータに分けるか、呼び出し順に意味を持たせる構成が適切です。デコレータの副作用フロー図
この図からも分かるように、複数責務が1つの装飾に集約されると、制御フローが可読性・保守性の観点で破綻しやすい。
デコレータの分離と合成による設計改善
改善例:責務ごとの分離
@require_admin
@log_call
@track_state
def execute():
...@require_admin:認可のみ@log_call:ログのみ@track_state:状態変更のみ
分離レビュー
@Reviewer: 装飾関数を責務単位に分割することで、各デコレータの目的が明確化され、個別に差し替え・テスト・再利用が可能になります。非同期関数への装飾対応
非対応例
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start}")
return result
return wrapper
@timer
async def fetch():
await some_io()この構造はawaitを正しく処理できない。レビューでは非同期関数に同期用デコレータが誤って使われていないか確認する必要がある。
改善:非同期専用デコレータ
def async_timer(func):
@wraps(func)
async def wrapper(*args, **kwargs):
start = time.time()
result = await func(*args, **kwargs)
print(f"{func.__name__} took {time.time() - start}")
return result
return wrapperデコレータのテスト可能性を担保する
良くない例:関数名で振る舞いが変わる
def audit(func):
def wrapper(*args, **kwargs):
if func.__name__.startswith("admin_"):
log_admin(func.__name__)
return func(*args, **kwargs)
return wrapperテスト性レビュー
@Reviewer: 関数名の文字列に依存して動作が変わる構造は、モック・テストの際に再現性が担保できません。明示的なフラグ等による制御へ変更を提案します。レビュー観点まとめ
| 観点 | 内容 | 優先度 |
|---|---|---|
| 副作用の有無 | I/O, DB更新, ログなどの操作が隠れていないか | 高 |
| 責務の一貫性 | 単一デコレータが複数の処理を抱え込んでいないか | 高 |
| 非同期対応 | async def に同期デコレータを使っていないか |
高 |
| functools.wrapsの使用 | 関数属性が適切に保存・転送されているか | 中 |
| テスト容易性 | 振る舞いが関数外の状態・文字列・タイミングに依存していないか | 中 |
まとめ:レビューアーは「装飾の透明性と分離性」を設計に戻す
デコレータは便利であるが、装飾された関数の実体が外から見えづらくなるというリスクを持つ。
レビューアーは、「何を足したのか」「どこに副作用があるのか」「なぜこの設計にしたのか」という装飾の意図と構造的明快さを読み解き、責務の整理と安全性の担保を行う立場にある。
見えない処理こそレビューすべき対象である。
