Python|try-except上級者向け設計ミスの見抜き方
この記事のポイント
- try-except構造における設計崩壊ポイントをレビューできる
- 例外処理の粒度設計・責務分離ミスを発見する力が身につく
- 例外漏洩、予期せぬ成功、誤捕捉パターンをコードから読み解く
そもそもtry-exceptとは
Pythonのtry-except
構文は、例外発生時に処理を分岐させる仕組みです。以下のような基本形がよく使われます。
try:
risky_operation()
except SomeException as e:
handle_exception(e)
これにより、エラー発生箇所とその処理を分けることができます。問題は、この便利さが過剰防御や設計ミスを招きやすいことです。
なぜこれをレビューするのか
実務でよく見るのは「とりあえずtry-exceptで囲っておく」コードです。これには以下の問題が内在します。
- エラー検出責務の混在
- 正常系フローの隠蔽
- 原因特定困難なcatch-all
- 失敗時の状態不整合
レビューアーは「例外は制御フローの代替手段ではない」という原則を頭に置きつつ、try-exceptが設計意図通り機能しているかを読み取る必要があります。
レビューアー視点
- try-exceptで何の責務をカバーしているのか
- 想定している例外種別は明示されているか
- except節の中身が妥当か(ログ、リトライ、巻き戻し)
- except節の粒度が適切か
- except節に副作用が隠れていないか
開発者視点
- 例外種別を事前に調査して明示すること
- 本来起きないはずの例外は潰さず上位に伝播させること
- except節の副作用を安易に積み上げないこと
- 例外をビジネスロジックの分岐代わりに使わないこと
良い実装例
なぜこの実装が良いのか
- 例外種別を
DatabaseError
に限定しており、責務の範囲が明確 - 原因情報をロギングしており、障害調査に役立つ情報を確保
- 例外を再送出することで上位層の障害監視が機能する
- try-exceptの粒度が最小限で、巻き取りすぎていない
- except節に副作用が存在せず、シンプルで読みやすい
# api_request_log_saver.py
import logging
from db_connection import insert_request_log, DatabaseError
class ApiRequestLog:
def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
self.request_id = request_id
self.endpoint = endpoint
self.client_ip = client_ip
self.response_code = response_code
self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
try:
insert_request_log(log)
except DatabaseError as e:
logging.error(f"DB error while saving request log: {e}")
raise
補足
再送出によって、障害監視・上位サービス層が異常検知できる設計になっています。ここでのexceptは責務範囲を超えていません。
レビュー観点
- 想定例外の明示有無
- catch-allの使用有無(
except:
,except Exception:
) - except内での副作用実装有無
- 状態巻き戻しや補正処理の要否検討
- 例外情報の欠落(ログ記録に例外詳細含める)
良くない実装例: ケース1
典型的なtry-except乱用パターンを提示します。
# request_logger.py
import logging
from db_connection import insert_request_log
class ApiRequestLog:
def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
self.request_id = request_id
self.endpoint = endpoint
self.client_ip = client_ip
self.response_code = response_code
self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
try:
insert_request_log(log)
except:
@Reviewer例外種別が不明確です。最低限 `except Exception:` 等で限定してください。 logging.warning("Failed to save request log")
@Reviewer例外原因をログに出力していません。障害調査困難になります。例外内容を記録してください。@Reviewer例外を上位に再送出しておらず、異常検知ができなくなります。原則再送出してください。
問題点
- catch-allで何でも吸収してしまっている
- 原因情報の欠落
- 障害伝播阻害
改善例
# request_logger_improved.py
import logging
from db_connection import insert_request_log, DatabaseError
class ApiRequestLog:
def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
self.request_id = request_id
self.endpoint = endpoint
self.client_ip = client_ip
self.response_code = response_code
self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
try:
insert_request_log(log)
except DatabaseError as e:
logging.error(f"DB error: {e}")
raise
except Exception as e:
logging.critical(f"Unexpected error while saving log: {e}")
raise
事前に発生可能性のあるDatabaseError
は通常系。想定外のException
はクリティカル扱いし通知強度を上げています。責務分離が整理できている例です。
良くない実装例: ケース2(副作用込みパターン)
次にtry-except内で副作用を抱え込む典型例です。
# request_logger_with_fallback.py
import logging
from db_connection import insert_request_log
class ApiRequestLog:
def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
self.request_id = request_id
self.endpoint = endpoint
self.client_ip = client_ip
self.response_code = response_code
self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
try:
insert_request_log(log)
except:
@Reviewer例外種別が不明確です。副作用処理を伴う場合は特に例外種を限定してください。 logging.warning("Failed to save request log")
@Reviewerログ記録のみならず副作用が混在しています。責務分離を検討してください。 send_alert_email("DB failed, log saving skipped")
retry_insert(log)
@Reviewerexceptブロックが肥大化しています。エラーハンドリング処理の分離が望ましいです。
問題点
- 副作用(通知・リトライ)とエラー処理が混在
- 捕捉粒度が曖昧
- 責務集中
改善例
# request_logger_separated.py
import logging
from db_connection import insert_request_log, DatabaseError
from notifications import send_alert_email
from retry import retry_insert
class ApiRequestLog:
def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
self.request_id = request_id
self.endpoint = endpoint
self.client_ip = client_ip
self.response_code = response_code
self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
try:
insert_request_log(log)
except DatabaseError as e:
handle_db_error(e, log)
def handle_db_error(e, log):
logging.error(f"DB error while saving request log: {e}")
send_alert_email("DB failed during request log save.")
retry_insert(log)
副作用ロジックはhandle_db_error
へ分離し、save_request_log
の責務を単純化しています。レビューアー視点でも読みやすい構造です。
観点チェックリスト
まとめ
try-exceptはあくまで「設計の表現」であって「保険」ではありません。レビューアーは例外構造=設計意図の写像であると読み解き、単なる構文ミスではなく、責務崩壊を発見することが重要です。安易な捕捉や副作用の混入を許さず、責務を整理するレビュー技術を磨いていきましょう。