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プログラミング vol.14 ~例外処理②~

Posted at

まえがき

Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。

対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。

ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
➡ 最近複数の方から、誤りのご指摘、編集リクエストによる修正/改良をいただいております。この場を借りてですが、感謝申し上げます。

今回も「例外処理」について扱います。それでは始めましょう!

Q.14-1

finally 節内で return を用いた場合、try 節や except 節の return がどのように上書きされるかを説明せよ。

問題背景

【1】 finally 節と return の関係

finally 節は、try 節と except 節の後に必ず実行される部分である。例外が発生したかどうかに関わらず実行されるため、リソースの解放やロギング、クリーンアップ処理などに使われる。
しかし、finally 節内で return 文を使用すると、try 節や except 節での return が上書きされることがある。そのため、finally 節内の return が最終的な戻り値として返されることになる。

【2】 実務的な使用例

・ リソース管理:ファイルのクローズやデータベースの接続終了など、リソースを確実に解放するために finally 節が使用される。
・ ログ出力:例外処理後のログ出力やデバッグ情報の記録など、関数がどのように終了したかを記録するために finally 節が利用される。

【3】finally 節の設計的利点

finally 節の設計的利点は以下のように挙げられる。
リソース管理:finally 節は、リソース(例えば、ファイルやデータベース接続)のクローズ処理を確実に行える。try 節や except 節でエラーが発生しても、後処理は必ず実行される。
整合性の保証:関数の制御フローにおいて、finally 節は最後に実行されることが保証されており、これにより、リソースや状態のクリーンアップを保証する。
スムーズなエラーハンドリング:finally 節は、エラー発生後も正常終了後も実行されるため、プログラムの後続処理が中断されることなく整合性を保つ。

【4】finally 節で return を用いる際の設計上の注意点

finally 節で return を使うことは推奨されない場合が多い。finally 節内での return は、予期せぬ動作を引き起こすことがあり、コードの可読性や意図が分かりにくくなる可能性がある。finally 節でリソースを解放する場合などは、return の使用を避け、処理結果を関数の終了時にまとめて返すことが望ましい。
具体的には、finally 節では、リソースの解放や状態の更新のみを行い、関数からの戻り値の設定は try 節または except 節で行うことが望ましい。例えば、関数内での状態更新を finally 節で行い、最終的な戻り値の設定は try 節で処理を行うようにする。

解答例とコードによる実践

【A】finally 節で return を使用した場合、try 節や except 節での return がどのように上書きされるかを示すコード

# 【1】関数を定義
def test_return():
    try:
        # 【2】try 節で return を実行
        print("try 節を実行")
        return "try return"
    except Exception as e:
        # 【3】except 節で return を実行
        print("except 節を実行")
        return "except return"
    finally:
        # 【4】finally 節で return を実行
        print("finally 節を実行")
        return "finally return"

# 【5】関数を実行
result = test_return()

# 【6】結果を出力
print(f"関数からの戻り値: {result}")
実行結果
try 節を実行
finally 節を実行
関数からの戻り値: finally return

【B】finally 節が return の前に必ず実行されることを示す例

# 【1】ログ出力モジュールをインポート
import logging

# 【2】ログ設定(INFO レベル以上を出力)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# 【3】関数定義
def example_function():
    try:
        # 【4】試行する処理(正常な処理)
        logging.info("処理を開始します。")
        return_value = 42  # 【5】正常系での return
        return return_value
    except Exception as e:
        # 【6】例外が発生した場合の処理(例外捕捉)
        logging.error(f"例外が発生しました: {e}")
    finally:
        # 【7】finally 節:必ず実行される後処理
        logging.info("finally 節が実行されました。")
        # 【8】return より先に実行されることを確認

# 【9】関数呼び出し
result = example_function()
logging.info(f"関数から戻り値: {result}")
実行結果
2025-05-01 22:10:00,123 - INFO - 処理を開始します。
2025-05-01 22:10:00,124 - INFO - finally 節が実行されました。
2025-05-01 22:10:00,125 - INFO - 関数から戻り値: 42

Q.14-2

finally 節が意図せず例外を握りつぶす(suppressed)原因となる構造を2つ挙げ、それを防ぐための設計指針を述べよ。

問題背景

【1】finally による例外の握り潰し

finally 節は try/except の後に必ず実行される構造であり、例外の有無にかかわらず後処理を保証するために用いられる。主な用途はファイルや接続の解放、ロックの解除、ログ出力など副作用の収束である。
tryexcept 節で発生した例外が finally 節内のreturn や例外で上書きされることで、元の例外情報が失われる状況を「例外の握り潰し」という。これによりデバッグが困難になり、バグの根本原因に到達できなくなる。

【2】握り潰しが発生する代表的な構造と防止策

【構造1】finally 節で return を用いることで例外が無視される

def example_suppressed_return():
    try:
        raise ValueError("元の例外")
    finally:
        return "上書きされたreturn"

問題点:ValueError が発生しているにも関わらず、finally 節で return を行うことで例外が握りつぶされる。
防止策:finally 節で return を使用しないか、return 先に例外情報を保持して処理する。

**【構造2】finally 節で新たに例外を発生させる

def example_suppressed_exception():
    try:
        raise ValueError("元の例外")
    finally:
        raise RuntimeError("finallyの新たな例外")

問題点:finally 節で新たな例外が投げられ、try 節の ValueError が完全に失われる。
防止策:finally 節での例外は、try/except でログを記録し、再スロー(raise)することで明示する。

実行計画とコードによる実践

【A】例外が握りつぶされるケース

# 【1】例外がfinally内のreturnで消える例
def suppressed_by_return():
    try:
        # 【2】意図的に例外を発生させる
        raise ValueError("元の例外")
    finally:
        # 【3】ここでのreturnにより、上記の例外は無視される
        return "returnによる例外の握りつぶし"

# 【4】関数呼び出しと戻り値の確認
result = suppressed_by_return()
print(f"戻り値: {result}")  # 出力: 戻り値: returnによる例外の握りつぶし
実行結果
戻り値: returnによる例外の握りつぶし

try 節で ValueError を発生させますが、finally 節の return によって例外が握りつぶされ、戻り値が返される。この場合、ValueError は発生せず、return の値("returnによる例外の握りつぶし")が返される。

【B】例外がfinally内の新たな例外で上書きされる例

# 【1】例外がfinally内の新たな例外で握りつぶされる例
def suppressed_by_new_exception():
    try:
        # 【2】元の例外を発生
        raise ValueError("元の例外")
    finally:
        # 【3】新たな例外により元の例外が失われる
        raise RuntimeError("finally内の新たな例外")

# 【4】例外がどうなるか確認
try:
    suppressed_by_new_exception()
except Exception as e:
    print(f"補足された例外: {e}")  # 出力: RuntimeError: finally内の新たな例外
実行結果
補足された例外: finally内の新たな例外

try 節で発生させた ValueError は、finally 節内で新たに発生した RuntimeError によって上書きされる。このため、元の ValueError は失われ、finally 節で発生した RuntimeError が最終的に補足される。

設計指針 説明
finally 内での return を避ける 関数の戻り値は try / except 節で処理し、後処理に return を使わないこと
finally 節で例外を起こすときはログと再スロー ログに残したうえで、raise によって明示的に例外を再スローすべき
例外情報を退避してから処理を行う try 節の例外を一時変数に保存し、finally 節で明示的に扱う

【C】例外の保存と明示的な再スロー(改善コード)

def safe_finally():
    exc = None  # 【1】例外保持用変数
    try:
        raise ValueError("元の例外")  # 【2】例外発生
    except Exception as e:
        exc = e  # 【3】例外を一時保存
        raise  # 【4】再スロー
    finally:
        print("後処理を実行")  # 【5】ここでreturnやraiseは行わない
        # 【6】必要に応じてexcの情報をログに記録
実行結果
後処理を実行

try 節で ValueError を発生させ、except 節でその例外をキャッチして一時変数 exc に保存する。raise によって例外を再スローし、finally 節で後処理が実行されます。finally 節内では returnraise は行わず、後処理を実行したあと、呼び出し元で再スローされた例外がキャッチされることが期待される。

補足事項

・ Python 3.11以降では except* 構文も登場し、複数例外のハンドリングが高度化しているが、finally 節の基本的な抑制挙動は変わらない。
contextlibwith 文による設計も、後処理を安全に行いたい場面で finally の代替となり得る。

本問のまとめ

finally 節での return または新たな raise により、tryexcept 節で発生した本来の例外が握りつぶされる可能性がある。これを防ぐには、finally 節内では状態の後始末やログ記録に留め、戻り値や例外処理は他の節で完結させるべきである。また、必要であれば例外情報を退避・再スローする構造により、安全かつ透明性のある制御フローを確保することが望ましい。

Q.14-3

Pythonで with 文が finally 節の代替となる構造であることを示し、両者の違いと使い分けについて論ぜよ。

問題背景

【1】 finally 節と with 文の関係

with 文はコンテキストマネージャー(enter/exit)を自動で呼び出す構造であり、後処理を自動化/明示化するために使用される。本質的には finally 節と同じく「必ず行うべき後処理の保証」を担う構造である。

【2】 使い分けと設計思想

with 文を使うべき場面としては、
・ ファイル操作、DB接続、ソケット通信、リソース制御などスコープが明確な副作用処理
・ 標準ライブラリまたは自作クラスがコンテキストマネージャに対応しているとき
が挙げられる。
一方で、finally 節が有効な場面としては以下、
・ コンテキストマネージャに対応していないリソースの後処理
・ 例外が起きようが起きまいが、必ず実行したい後処理が存在する場合
・ 複数の後処理を共通化したい場合(たとえば複数のログ・リリース処理など)

解答例とコードによる実践

【A】read_file_finally() 関数の実行結果

# 【1】ファイルを読み取る関数(finally版)
def read_file_finally(filename):
    try:
        # 【2】ファイルを開く
        file = open(filename, "r")
        # 【3】ファイル内容を読み込む
        content = file.read()
        return content  # 【4】正常に読み込んだら返す
    finally:
        # 【5】ファイルが開かれていたら閉じる(必ず実行)
        file.close()

# 【6】関数呼び出しと出力
print(read_file_finally("sample.txt"))
実行結果(ファイルが存在する場合)
This is a sample text file.
実行結果(ファイルが存在しない場合)
FileNotFoundError: [Errno 2] No such file or directory: 'sample.txt'

【B】 read_file_with() 関数の実行結果

# 【1】ファイルを読み取る関数(with版)
def read_file_with(filename):
    # 【2】with文を使うことで、__enter__と__exit__が自動で呼ばれる
    with open(filename, "r") as file:
        # 【3】ファイル内容を読み込んで返す
        return file.read()  # 【4】終了時にfile.close()が自動実行される

# 【5】関数呼び出しと出力
print(read_file_with("sample.txt"))
実行結果(ファイルが存在する場合)
This is a sample text file.
実行結果(ファイルが存在しない場合)
FileNotFoundError: [Errno 2] No such file or directory: 'sample.txt'

【C】カスタムコンテキストマネージャ

# 【1】自作のコンテキストマネージャ(ロギング用)
class LogContext:
    def __enter__(self):
        print("処理開始")
        return self  # 【2】任意のオブジェクトを返してもよい
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("後処理(ログ出力)")  # 【3】finallyに相当する処理

# 【4】使用例
with LogContext():
    print("主処理を実行中")
実行結果
処理開始
主処理を実行中
後処理(ログ出力)

本問のまとめ

Pythonにおける with 文は、finally 節と同様に後処理の保証を目的とする構造であるが、その実装はより構造的・安全かつ可読性が高いという点で優れている。ただし、すべての場面に適用可能なわけではなく、__enter__ / __exit__ を実装できないケースや複雑なエラーハンドリングが必要な場合には、finally 節の使用が適している。適切な場面でこれらを使い分けることで、例外安全性とコード品質の両立が可能となる。

Q.14-3

raise によって送出された例外が、呼び出し階層のどこまで伝播するかを制御する設計方針について論ぜよ。

問題背景

【1】 例外(Exception) と raise

Pythonでは、プログラムの実行中に予期しないエラー(例外)が発生することがある。例外は、プログラムが意図しない状況に遭遇した場合に、エラーハンドリングを行うためのメカニズムである。
raise 文を用いると、指定した例外を手動で発生させることが出来る。この例外は呼び出し元に伝播され、適切に処理されるまで止まることはない。

raise Exception("エラーメッセージ")

この raise 文によってプログラムの制御はその場所で終了し、try ブロック内の例外が捕捉されるまで次の行は実行されない。

【2】raise による再スロー

except ブロック内で補足した例外をサイドスローすることで、その例外を上位の try-except に伝播させることが出来る。この動作は、エラーハンドリングを複数の層で行いたい場合に便利。

def outer_function():
    try:
        inner_function()
    except Exception as e:
        print(f"外部関数でエラーが発生しました: {e}")
        raise  # 例外を再スロー

raise は、通常の例外の送出だけでなく、現在のexceptブロックでキャッチした例外を再スローすることができる。再スローした例外は、さらに上位のtry-exceptに伝播する。

【3】else ブロックと finally ブロック

else ブロックは、try ブロック内の処理が正常に完了した場合にのみ実行されるコード。もし try ブロック内で例外が発生した場合、else ブロックは実行されない。これにより、正常終了時にのみ行いたい処理(例:成功した場合のログ出力)を分離できる。
finally ブロックは、try ブロック内で例外が発生したかどうかに関わらず、必ず実行されるコードを記述する場所。finally は、リソースの解放(例えば、ファイルのクローズやネットワーク接続の切断)や、プログラムの後処理を保証するために使用。

try:
    file = open("example.txt", "r")
    content = file.read()
except FileNotFoundError:
    print("ファイルが見つかりません")
finally:
    file.close()  # 必ずファイルを閉じる

このコードでは、try ブロック内でファイルの読み込みを試み、エラーが発生しても finally ブロックでファイルを閉じる。finally は、エラーの有無に関わらず必ず実行されるため、リソースの解放を確実に行いたいときに非常に有用。

【4】例外伝播と再スローによる設計

raise 文は例外を送出するために使用されるが、この例外がどこまで伝播するかは、try-except 構文と組み合わせることで制御できる。例えば、複数の関数が絡むような構造では、各関数内で発生した例外を捕捉し、それを適切に上位の関数に伝播させることができる。これにより、例外を適切に管理し、エラーハンドリングを分離することが可能になる。

def process_data(data):
    try:
        # 【1】データが "error" の場合、ValueErrorを発生させる
        if data == "error":
            raise ValueError("無効なデータ")
        # 【2】正常に処理された場合のメッセージを返す
        return f"処理成功: {data}"
    except ValueError as e:
        # 【3】ValueErrorが発生した場合、エラーメッセージを出力
        print(f"内部エラー: {e}")
        # 【4】捕捉したエラーを再スローして、上位に伝播
        raise

def main():
    try:
        # 【5】process_data関数を呼び出し、"error"を渡す
        result = process_data("error")
        # 【6】正常な結果が返ってきた場合、結果を表示(実際には処理されない)
        print(result)
    except ValueError as e:
        # 【7】process_dataで発生したValueErrorを捕捉し、最終的なエラーメッセージを表示
        print(f"最終的なエラー処理: {e}")

# 実行
main()
実行結果
内部エラー: 無効なデータ
最終的なエラー処理: 無効なデータ

このコードは、process_data 関数内で発生した ValueErrormain関数に伝播させ、最終的に main 関数内で処理する流れになる。try-except 構文を用いることで、エラー発生時に適切に処理を分け、再スローによって上位の処理にエラーを伝えることができる。

解答例とコードによる実践

# 【1】自作例外クラスを定義する
class CustomError(Exception):
    def __init__(self, message):
        self.message = message
        super().__init__(self.message)

# 【2】外部関数で例外を送出する
def outer_function():
    try:
        # 【3】内部関数で例外を発生させる
        inner_function()
    except CustomError as e:
        # 【4】外部関数で例外を捕捉し、再スロー
        print(f"外部関数で例外を捕捉しました: {e}")
        raise  # 【5】再スローして例外を上位に伝播

# 【6】内部関数で例外を発生させる
def inner_function():
    # 【7】例外を送出
    raise CustomError("カスタムエラーが発生しました")

# 【8】例外を呼び出し元で処理する
try:
    outer_function()
except CustomError as e:
    print(f"最上層で例外を処理しました: {e}")
実行結果
外部関数で例外を捕捉しました: カスタムエラーが発生しました
最上層で例外を処理しました: カスタムエラーが発生しました

設計上のポイント

・ 例外伝播の制御:raise を使って例外を伝播させることで、エラーハンドリングを一貫性を持たせて行う。例外は必要な階層で適切にキャッチし、再スローして他の階層でさらに処理を続けることができる。
・ エラーメッセージの伝達:カスタム例外クラスを使うことで、エラーの詳細を明確に伝えることができ、問題の診断がしやすくなる。
・ 再スロー:except 節で捕捉した例外を再スローすることで、エラーハンドリングを特定の階層で終わらせず、上位階層に伝播させることができる。

Q.14-4

raise 命令を例外補足構造の中でのみ用いる設計と、平常処理の一部で用いる設計の違いと、その安全性について述べよ

問題背景

【1】raise 文を例外捕捉内で用いる設計

raise を例外捕捉構造(try-except)内で用いる場合、発生した例外をキャッチしてから、再度その例外を送出する設計となる。この手法は、上位の関数に例外を伝播させるために使用され、エラー処理の階層化を可能にする。

利点
エラーの伝播:上位の処理にエラー情報を伝え、上位で適切に処理を行うことができる。
エラーの一元管理:エラーが発生した場合、適切にログを記録したり、他のリソースをクリーンアップするための処理を行ってからエラーを再送出することができる。
可読性の向上:エラーが発生した場所と、それを補足して処理する場所が明確に分かれるため、コードの可読性が向上する。

欠点
例外が二重に発生する可能性:例外を捕まえた後に再スローする際、二重に例外が発生する可能性がある。これを避けるために、再スローする例外を選択的に変更する必要がある。

def process_data(data):
    try:
        # 【1】dataが"error"の場合、ValueErrorを発生させる
        if data == "error":
            raise ValueError("無効なデータ")
        return f"処理成功: {data}"

    except ValueError as e:
        # 【2】例外をキャッチし、エラーメッセージを表示
        print(f"内部エラー: {e}")
        # 【3】例外を再スローして上位に伝播させる
        raise  

def main():
    try:
        # 【4】process_data関数を呼び出し、"error"を渡す
        result = process_data("error")
        print(result)
    except ValueError as e:
        # 【5】再スローされた例外をキャッチし、最終的なエラーメッセージを表示
        print(f"最終的なエラー処理: {e}")

# 実行
main()
実行結果
内部エラー: 無効なデータ
最終的なエラー処理: 無効なデータ

【2】raise を平常処理の一部として用いる設計

raise を平常処理(通常の業務ロジックの中)で使う場合、予期しない条件に対して即座に例外を発生させ、処理を中断させる設計となる。この手法は、例えば不正な入力や、前提条件が満たされていない場合に使用される。
利点
早期検出: エラー条件が発生した時点で即座に例外を発生させるため、問題を早期に発見し、未然に問題を防ぐことができる。
コードがシンプル: 必要な場所で即座にエラーを送出するため、他のエラーハンドリングコードが複雑にならず、シンプルで分かりやすい。
欠点
エラーハンドリングが不完全: 例外が発生した時点で処理が中断されるため、後続の処理やリソースの解放が行われない可能性がある。
例外の乱用: 業務ロジックの中で raise を乱用すると、エラー管理が複雑になり、可読性や保守性が低下する。

def process_data(data):
    # 【1】dataが"error"の場合、ValueErrorを発生させる
    if data == "error":
        raise ValueError("無効なデータ")
    return f"処理成功: {data}"

def main():
    try:
        # 【2】process_data関数を呼び出し、"error"を渡す
        result = process_data("error")
        print(result)
    except ValueError as e:
        # 【3】ValueErrorをキャッチし、エラーメッセージを表示
        print(f"エラー処理: {e}")

# 実行
main()
実行結果
エラー処理: 無効なデータ

【3】安全性の違いと設計の使い分け

例外補足構造内での raise: この設計は、上位の処理で例外を補足し、適切に対処できるため、システム全体の安定性が保たれる。エラーをキャッチした後、必要に応じてログを記録したり、クリーンアップ処理を実行したりできる。
このような raise は、エラー管理を体系的に行い、システムの信頼性を高めるために使用すべきである。特に大規模なシステムでは、エラーがどこで発生したかを追跡し、適切に対応することが重要である。
平常処理内での raise: 予期しないエラーを早期に発見し処理を停止する点では有効だが、エラー発生時に後続処理がスキップされるため、リソースの解放やエラーログの記録を行わずに中断される可能性がある。これにより、システムの整合性が損なわれるリスクが高まる。
このような raise は、予期しない事態を即座に中断し、エラーが拡大しないように防ぐために使用すべきである。特に、ユーザー入力の検証やデータ整合性を保つ場面で役立つ。

解答例とコードによる実践

上記の問題解説にて、解答は尽きているので省略する。

Q.14-5

例外の補足と raise による再送出を繰り返す構造において、例外が過度にネストされる設計の問題点と、その回避策を述べよ。

問題背景

【1】raise による例外の再送出

raise 文を使うことで、捕捉した例外を再送出することができる。これにより、例外が発生した箇所から上位の処理にエラー情報を伝えたり、エラーを再処理することが可能になる。

try:
    try:
        # ゼロ除算エラー
        result = 10 / 0
    except ZeroDivisionError as e:
        print(f"内部エラー: {e}")
        raise  # 例外を再送出する
except ZeroDivisionError as e:
    print(f"再送出されたエラー: {e}")

【2】過度にネストされた例外設計の問題点

過度にネストされた例外処理は以下の問題を引き起こす可能性がある:

  1. 可読性の低下:複数の try-except ブロックがネストされることで、どのエラーがどの位置で発生したのかが追いにくくなる。また、エラーハンドリングのロジックが複雑になり、コードの理解が難しくなる。
  2. デバッグの困難さ:例外が何度も再送出されることで、どの段階でエラーが発生したのかが不明確になり、問題の特定と修正が難しくなる。
  3. 冗長な処理:再送出の繰り返しにより、エラー処理が冗長化する。これにより、パフォーマンスが低下したり、エラーの根本的な原因が解決されないことがある。

【4】回避策

過度にネストされた例外処理を回避するためには、以下の方針が有効である:

  1. 例外の集約:複数のエラータイプを同一の except ブロックで処理し、エラーハンドリングを簡潔にする。
  2. エラーメッセージの明確化:再送出する際には、エラーメッセージをより具体的にして、エラーの発生場所と原因を追跡しやすくする。
  3. エラー処理の最適化:エラーハンドリングは必要最小限にとどめ、不要な再送出や多重処理を避ける。

解答例とコードによる実践

# 【1】簡潔にエラーハンドリングを行う関数
def process_data(data):
    try:
        # 【2】データが整数でない場合にエラーを発生させる
        if not isinstance(data, int):
            raise TypeError("データは整数でなければならない")
        # 【3】ゼロ除算を避ける処理
        result = 10 / data
    except (TypeError, ZeroDivisionError) as e:
        # 【4】エラーメッセージを表示し、エラーを再送出
        print(f"エラー発生: {e}")
        raise  # エラーを再送出する
    return result  # 正常終了

# 【5】外部からのデータ処理要求
try:
    data = "文字列"  # 整数ではないデータ
    process_data(data)  # ここでエラーが発生
except Exception as e:
    # 【6】最終的なエラーハンドリング
    print(f"最終的なエラー処理: {e}")
実行結果
エラー発生: データは整数でなければならない
最終的なエラー処理: データは整数でなければならない

まとめ

過度にネストされた例外処理は、コードの可読性を低下させ、デバッグを困難にする原因となる。再送出を繰り返す場合、エラーメッセージを具体的にし、エラーハンドリングをシンプルに保つことが重要である。また、エラーハンドリングを過剰に行わず、エラーが発生した場合に適切に処理し、最終的にエラーがどのように伝達されているかを明確にすることが求められる。

Q.14-6

raise from 構文を用いた例外チェーンが、デバッグ・ロギングの観点から有効となる理由と、それが逆効果になるケースを述べよ。

問題背景

【1】raise from 構文

Pythonでは、例外を捕捉して新たに別の例外を発生させる際に、raise from構文を使用することができる。これにより、元の例外と新たに発生させた例外の間に「例外チェーン」を作成することができ、エラーが発生した場所とその原因をより明確に追跡することができる。

try:
    # 何らかのエラーを発生させるコード
    result = 10 / 0
except ZeroDivisionError as e:
    # 新たな例外を発生させ、元の例外を保持する
    raise ValueError("値の計算に失敗しました") from e

【2】デバッグとロギングの観点から有効な理由

raise from構文を用いることで、複数の例外が関連していることを明示的に示すことができる。この構文により、元の例外がどこで発生したのか、またそれに関連する新たな例外がどこで発生したのかを追跡することができ、デバッグが容易になる。
デバッグの際に、複数の例外がどのように関連しているのかを理解するためには、例外チェーンが役立つ。元の例外が新たな例外に関連していることを示すことで、エラーが発生した一連の流れを把握しやすくなる。

【3】逆効果になるケース

逆効果になるケースとしては、次のような場合が考えられる:
過剰な例外チェーン:すべての例外に対して無差別に raise from を使うと、不要に複雑な例外チェーンが生成され、エラーメッセージが過剰に長くなる。これにより、最初に発生した本来のエラーに関する情報が埋もれてしまうことがある。
例外チェーンが意味を成さない場合:例えば、まったく関係のない異なる種類の例外同士を raise from で結びつけると、原因と結果の関係が不明瞭になり、逆にデバッグが難しくなることがある。

解答例とコードによる実践

# 【1】簡単な関数で例外チェーンを発生させる
def process_data(data):
    try:
        # 【2】データが整数でない場合、エラーを発生させる
        if not isinstance(data, int):
            raise TypeError("データは整数である必要があります")
        # 【3】ゼロ除算エラーを発生させる
        result = 10 / data
    except (TypeError, ZeroDivisionError) as e:
        # 【4】新たな例外を発生させ、その原因となる元の例外を`raise from`で保持する
        raise ValueError("データの処理に失敗しました") from e
    return result

# 【5】外部で関数を呼び出し、例外が発生する場合
try:
    data = "文字列"  # 整数でないデータ
    process_data(data)  # ここでエラーが発生
except Exception as e:
    # 【6】例外チェーンが表示される
    print(f"最終的なエラー処理: {e}")
実行結果
最終的なエラー処理: データの処理に失敗しました
Caused by: TypeError: データは整数である必要があります

本問のまとめ

raise from構文を用いた例外チェーンは、デバッグやロギングの際に非常に有用である。原因と結果の関係を明示的に示し、エラーがどのように発生したかを追跡するのに役立つ。しかし、過剰に例外チェーンを使用するとエラーメッセージが冗長化し、デバッグがかえって難しくなることがあるため、適切に使い分けることが重要である。

Q.14-7

raiseで送出される例外にローカル変数の値を含めたい場合、どのような形式で情報を渡すのが適切かを設計観点から論ぜよ。

問題背景

【1】raiseによる例外送出

Pythonでは、raise文を使って新たな例外を送出することができる。通常、例外にはエラーメッセージや情報が含まれるが、ローカル変数の値などの追加情報を例外に含めたい場合には、その情報をどうやって例外オブジェクトに渡すかを設計する必要がある。

try:
    # エラーが発生する処理
    raise ValueError("エラーメッセージ")
except ValueError as e:
    print(e)  # エラーメッセージを表示

【2】ローカル変数の値を例外に含める方法

例外を送出する際に、ローカル変数の値を含めたい場合、エラーが発生した状態でその変数の情報を記録する必要がある。これにはいくつかの方法がある:
カスタム例外クラスを使う:Exception を継承したカスタム例外クラスを作り、変数の値をそのクラスに渡す。
args に追加情報を含める:標準の例外クラスに渡す引数として変数を渡す。
__str__ メソッドをオーバーライドする:カスタム例外の__str__ メソッドを使って、エラー時にローカル変数を含むメッセージを表示する。

解答例とコードによる実践

ローカル変数を例外に渡す最も一般的な方法は、カスタム例外クラスを定義して、そのクラスにローカル変数を渡すことである。これにより、例外が発生した際に、変数の情報を簡単に含めることができる。

# 【1】カスタム例外クラスを定義する
class MyCustomError(Exception):
    def __init__(self, message, value):
        super().__init__(message)  # 親クラスのコンストラクタを呼び出し
        self.value = value  # ローカル変数をインスタンス変数に保存

    def __str__(self):
        # 【2】エラーメッセージと一緒にローカル変数の値を表示
        return f"{super().__str__()} | ローカル変数の値: {self.value}"

# 【3】関数内でエラーが発生した場合に例外を送出する
def process_data(data):
    if data < 0:
        # 【4】カスタム例外を送出し、ローカル変数を含める
        raise MyCustomError("負の値は許可されていません", data)
    return data

# 【5】関数を呼び出し、例外をキャッチして表示する
try:
    process_data(-10)  # ここでエラーが発生
except MyCustomError as e:
    # 【6】例外と一緒にローカル変数の情報が表示される
    print(f"エラー発生: {e}")
実行結果
エラー発生: 負の値は許可されていません | ローカル変数の値: -10

まとめ

ローカル変数の値を例外に含めるための最もPythonicな方法は、カスタム例外クラスを作成してそのクラスに必要な情報を渡すことである。この方法を使うと、エラーメッセージだけでなく、エラーが発生した際の状態(例えば変数の値)も簡単に含めることができる。__str__ メソッドをオーバーライドすることで、エラーメッセージとローカル変数の値を適切に表示でき、デバッグが容易になる。

あとがき

今回も「例外処理」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回内容は未定です。

参考文献

[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)

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?