Python|contextlib.suppressの適切な使い方と設計ミスの防ぎ方
この記事のポイント
- contextlib.suppressの設計意図を理解してレビューに活かせる
- suppressの誤用による責務崩壊を発見できる
- 副作用吸収、原因消失、握り潰し設計を読み解く実践視点が身につく
そもそもcontextlib.suppressとは
Python標準ライブラリcontextlib
のsuppress
は、指定例外のみを吸収して処理を継続するためのコンテキストマネージャです。
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("somefile.tmp")
指定例外が発生した場合だけ例外を握り潰し、その他の例外はそのまま伝播させる仕組みになっています。
- 例外の意図的吸収
- finallyやtry-exceptの簡潔化
- 事前存在チェック不要なケースで活躍
強力である反面、設計意図が不明瞭になるリスクを抱えます。
なぜこれをレビューするのか
suppressの乱用は以下の設計崩壊を招きます。
- 原因不明の動作継続
- 期待外の例外吸収
- 障害調査不能化
- 抑制例外の責務不一致
レビューアーは「何の責務の例外を、どこで吸収すべきか」を常に問い直す必要があります。
レビューアー視点
- suppress対象例外が妥当か
- 吸収する責務が設計的に正当か
- 原因例外を握り潰していないか
- ログ・監視に影響していないか
- 安全に無視できる例外に限定されているか
開発者視点
- 例外原因と再現性を把握したうえで抑制する
- 想定外例外を吸収しない
- ログ抑制と動作抑制の責務を混在させない
- 「一時的な存在確認回避」用途を超えて乱用しない
- 複雑な抑制は通常のtry-exceptで責務分離する
良い実装例
なぜこの実装が良いのか
- ファイル削除で既に存在有無が意味を持たない設計
- suppress対象を
FileNotFoundError
に限定 - ログが不要な一時ファイル削除責務として自然
- 動作継続性を設計として保証
# temp_file_cleaner.py
import os
from contextlib import suppress
def cleanup_temp_file(filepath):
with suppress(FileNotFoundError):
os.remove(filepath)
補足
「消えていても問題ない」という業務責務に沿った吸収であり、 suppressの自然な使い方です。設計者の意図が明確です。
レビュー観点
- suppressの対象例外が限定的か
- 想定以外の例外吸収が起きていないか
- 抑制理由がコード上で説明できるか
- 動作継続が設計上許容可能か
- 副作用的な例外吸収責務になっていないか
良くない実装例: ケース1(対象例外の広すぎる吸収)
# bad_suppress_too_broad.py
import os
from contextlib import suppress
def cleanup_temp_file(filepath):
with suppress(Exception):
@ReviewerException全体を抑制対象にしており、障害の握り潰しになります。対象例外を限定してください。 os.remove(filepath)
@Reviewer副作用によりPermissionErrorやIOErrorなど本来検出すべき障害も吸収されてしまいます。
問題点
- Exception全体を吸収対象
- 障害系まで握り潰して原因不明化
- suppress本来の限定吸収の設計意図を逸脱
改善例: 抑制対象例外の限定
# good_suppress_limited.py
import os
from contextlib import suppress
def cleanup_temp_file(filepath):
with suppress(FileNotFoundError):
os.remove(filepath)
「設計上消えていて良い」ケースにのみ限定適用がsuppressの基本哲学です。レビューアーはこの一点を崩していないか注視します。
良くない実装例: ケース2(suppress責務の混在・副作用化)
# bad_suppress_sideeffect.py
import os
import logging
from contextlib import suppress
def cleanup_temp_file(filepath):
with suppress(FileNotFoundError):
os.remove(filepath)
logging.info(f"{filepath} を削除しました")
@Reviewersuppress内で副作用ロジック(ログ出力)を行うと、実行結果が不確定になります。責務を分離してください。
問題点
- suppressブロック内に正常系副作用を混在
- 例外発生時にログ出力が行われない矛盾
- 動作保証と抑制責務が混ざって可読性低下
改善例: suppressブロックを最小限に限定
# good_suppress_separated.py
import os
import logging
from contextlib import suppress
def cleanup_temp_file(filepath):
with suppress(FileNotFoundError):
os.remove(filepath)
logging.info(f"{filepath} を削除しました(存在有無問わず)")
suppressは「吸収だけ」に集中し、副作用は外に出す設計が原則です。レビューでよく崩壊するポイントなので指摘の優先度が高いです。
良くない実装例: ケース3(抑制責務を分岐代わりに濫用)
# bad_suppress_logic_abuse.py
import os
from contextlib import suppress
def ensure_temp_file(filepath):
with suppress(FileNotFoundError):
os.remove(filepath)
with suppress(FileNotFoundError):
os.mkdir(filepath)
@Reviewersuppressを分岐代わりに多用すると制御構造の意図が不明瞭になります。正常系と異常系を整理してください。
問題点
- suppressがフロー制御の代替手段になっている
- 正常系の存在確認責務が埋没
- 調査・維持が困難化
改善例: 通常分岐で責務整理
# good_suppress_logic_separation.py
import os
def ensure_temp_file(filepath):
if os.path.exists(filepath):
os.remove(filepath)
os.mkdir(filepath)
存在確認は通常分岐のほうが自然です。 suppressは「意図的に握り潰すべき異常だけ」を担当させます。
観点チェックリスト
まとめ
suppress
は非常に便利で記述量を削減できる分、設計意図の曖昧さがコードに紛れ込みやすい構文でもあります。
レビューアーは「責務が何を握り潰してよいか」を常に問い続け、吸収設計そのものが正当化できるのか読み解く技術を鍛えていきましょう。
特に副作用混在、対象例外の肥大化、分岐代替はレビュー頻出指摘ポイントです。 suppressレビューは現場育成でも非常に効果的な教材になります。