まえがき
Python3.13環境を想定した演習問題を扱っています。変数からオブジェクト指向まできちんと扱います。続編として、データサイエンティストのためのPython(NumPyやpandasのような定番ライブラリ編)や統計学・統計解析・機械学習・深層学習・SQLをはじめCS全般の内容も扱う予定です。
対象者は、[1]を一通り読み終えた人、またそれに準ずる知識を持つ人とします。ただし初心者の方も、文法学習と同時並行で進めることで、文法やPythonそれ自体への理解が深まるような問題を選びました。前者については、答えを見るために考えてみることをお勧めしますが、後者については、自身と手元にある文法書を読みながら答えを考えるというスタイルも良いと思います。章分けについては[1]および[2]を参考にしました。
ストックしている問題のみすべて公開するのもありかなと考えたのですが、あくまで記事として読んでもらうことを主眼に置き、1記事1トピック上限5問に解答解説を付与するという方針で進めます。すべての問題を提供しない分、自分のストックの中から、特に読むに値するものを選んだつもりです。
また内容は、ただ文法をそのままコーディング問題にするのではなく、文法を俯瞰してなおかつ実務でも応用できるものを目指します。前者については世の中にあふれていますし、それらはすでに完成されたものです(例えばPython Vtuberサプーさんの動画[3]など)。これらを解くことによって得られるものも多いですし、そのような学習もとても有効ですが、私が選んだものからも得られるものは多いと考えます。
私自身がPython初心者ですので、誤りや改善点などがあればご指摘いただけると幸いです。
➡ 最近複数の方から、誤りのご指摘、編集リクエストによる修正/改良をいただいております。この場を借りてですが、感謝申し上げます。
今回から「例外処理」について、何回かにわたって扱います。それでは始めましょう!
Q.13-1
try
節で open()
によるファイル読み込み処理を行い、ファイルが存在しない場合には FileNotFoundError
を捕捉し、ログとして "Missing File"
を出力するよう設計せよ。
問題背景
【1】open()
関数と例外の可能性
Pythonの open()
関数は、ファイルの読み込み/書き込みに使用される基本的な関数。しかし、指定されたファイルが存在しない場合やパスが不正である場合には、FileNotFoundError
(OSError
のサブクラス)が発生する。この例外を明示的に処理しなければ、プログラムはクラッシュする。
【2】try-except
文の目的
try
ブロックは、エラーが発生する可能性のある処理を記述する場所で、except FileNotFoundError:
により、ファイルが存在しないという特定のエラーを安全に細く・処理することが出来る。else:
や finally:
節と組み合わせることで、処理の安全性と明確な流れを担保できる。
【3】logging
モジュールによるエラーログの出力
print()
ではなく logging
を用いることで、ログレベル(INFO, WARNING, ERROR)を制御でき、実運用でも使える出力が可能となる。例外処理において logging.warning()
や logging.error()
を使うことで、例外内容や原因を記録に残せる。
解答例とコードによる実践
# 【1】ログ出力のため logging モジュールを読み込む
import logging
# 【2】ログ設定:INFO 以上を出力、ログの形式も指定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】読み込む対象のファイル名(存在しないファイルを指定してテスト)
filename = "nonexistent_file.txt"
# 【4】ファイル読み込みを試みる(例外が出る可能性あり)
try:
# 【5】ファイルを読み込みモードで開く
with open(filename, "r") as file:
# 【6】ファイルの内容を読み込んで表示(今回は省略)
contents = file.read()
logging.info("ファイルを正常に読み込みました。")
# 【7】ファイルが存在しない場合の処理
except FileNotFoundError:
# 【8】エラーをログとして記録
logging.warning("Missing File: 指定されたファイルが存在しません。")
# 【9】その他の例外を捕捉したい場合には以下のように拡張可能(今回は省略)
# except Exception as e:
# logging.error(f"予期しないエラーが発生しました: {e}")
2025-05-01 19:55:01,123 - WARNING - Missing File: 指定されたファイルが存在しません。
本問のポイントは以下
・ try–except
構文により、open()
のような失敗する可能性のある処理を安全に記述できる。
・ 特定の例外(FileNotFoundError
)をピンポイントで捕捉することで、異常系の原因を明確に制御し、ログで記録できる。
・ logging
を使ったエラー通知は、実運用における診断・デバッグ・モニタリングの基盤となる設計である。
本問のような構造は、設定ファイルの読み込み、テンプレートファイルの参照、データファイルの検証などに頻用される基本設計である。
ちなみに以下は、指定された2つのコード(os.path.exists()
による存在確認、および FileNotFoundError
の詳細をログに記録する構文)を既存のコードに組み込んだ統合バージョン
# 【1】ログ出力のため logging モジュールを読み込む
import logging
# 【2】os モジュールを読み込む(ファイル存在チェック用)
import os
# 【3】ログ設定:INFO 以上を出力、ログの形式も指定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【4】読み込む対象のファイル名(存在しないファイルを指定してテスト)
filename = "nonexistent_file.txt"
# 【5】ファイルが存在するかどうかを事前にチェックする
if os.path.exists(filename):
# 【6】ファイルが存在する場合は open() で読み込む
try:
# 【7】ファイルを読み込みモードで開く
with open(filename, "r") as file:
# 【8】ファイルの内容を読み込む(今回は省略)
contents = file.read()
logging.info("ファイルを正常に読み込みました。")
except FileNotFoundError as e:
# 【9】万が一存在していたはずのファイルが消えていた場合(レースコンディション)
logging.warning(f"Missing File: {e}")
else:
# 【10】ファイルが最初から存在しない場合のログ出力
logging.warning("Missing File: ファイルが存在しません。")
組み込んだ要素 | 機能と意義 |
---|---|
os.path.exists() |
ファイルの存在チェックを明示的に行い、不要な例外発生を未然に防ぐ構造である。 |
except FileNotFoundError as e |
ファイル削除などによる実行時競合(例:チェック後に削除された)を安全に補足し、詳細なログを記録できる構造である。 |
「事前検証による予防的設計」と「例外補足による冗長耐性」を両立した堅牢な構成である。現実的なファイル操作(特に並行処理や一時ファイル処理)において、os.path.exists()
と try–except
の併用は重要な実装パターンである。ログに明示的なエラーメッセージを記録することで、診断・監査・保守がしやすい構造となっている。
Q.13-2
ネストされた try-except
を含む関数において、内側の例外は補足して継続処理、外側の例外は再スローするよう明確に責任分離せよ。
問題背景
【1】 try–except
の基本構造と再スロー
Pythonでは、try
ブロック内で例外が発生した際に except 節で補足し処理を継続できる。raise
文を使うことで、補足した例外を再スロー(再送出)し、呼び出し元や外側のスコープに伝搬できる。これにより、例外処理の責任を階層ごとに分離して記述する設計(責任の明示)が可能となる。
【2】 ネストされた try–except
による階層的な制御
内側の例外(局所的な処理の失敗)は、原則としてその場でログを出して処理を継続すべきである。外側の例外(構造的または設計的なエラー)は、上位へ通知するために再スロー(raise)すべきである。これにより、プログラム全体の堅牢性と可観測性が向上する。
【3】 Pythonicな責任分離設計の意義
内側ではローカルな失敗(例:データ不正)を補足・記録し、処理を続行。外側では本質的に処理継続ができない重大な例外(例:外部接続失敗、ファイル破損)を再スロー。この設計により、「復旧可能なエラー」と「致命的エラー」の分類と責任が明示化される。
# 【1】ログ出力モジュールを読み込む
import logging
# 【2】ログ設定(INFOレベル以上を出力)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】データ項目を処理する関数(ネストされた例外制御を持つ)
def process_data_items(data_list):
# 【4】外側の try-except による全体処理の監視
try:
# 【5】各データ項目に対して処理を行う
for index, item in enumerate(data_list):
try:
# 【6】None はエラーとする例
if item is None:
raise ValueError("データがNoneである")
# 【7】正常な処理
logging.info(f"[{index}] データ '{item}' を処理中")
except ValueError as ve:
# 【8】内側の例外は補足してログに出し、継続する
logging.warning(f"[{index}] 無効なデータをスキップ:{ve}")
continue # 【9】次の項目へ
except Exception as e:
# 【10】外側で発生したその他の例外は再スロー(責任を委譲)
logging.error(f"致命的なエラー:{e}")
raise # 【11】再スローで呼び出し元に伝える
# 【12】メイン処理の定義
def main():
# 【13】処理対象のデータ(None を含む)
data = ["apple", None, "banana", "cherry", None, "date"]
# 【14】関数呼び出し
process_data_items(data)
# 【15】スクリプトのエントリーポイント
if __name__ == "__main__":
try:
main()
except Exception as e:
# 【16】再スローされたエラーを最終的に処理(例:ユーザー通知・停止)
logging.critical(f"プログラム停止:未処理の例外 {e}")
2025-05-01 20:10:01,123 - INFO - [0] データ 'apple' を処理中
2025-05-01 20:10:01,124 - WARNING - [1] 無効なデータをスキップ:データがNoneである
2025-05-01 20:10:01,125 - INFO - [2] データ 'banana' を処理中
2025-05-01 20:10:01,126 - INFO - [3] データ 'cherry' を処理中
2025-05-01 20:10:01,127 - WARNING - [4] 無効なデータをスキップ:データがNoneである
2025-05-01 20:10:01,128 - INFO - [5] データ 'date' を処理中
範囲 | 処理内容 | 方針 |
---|---|---|
内側の try
|
ValueError を補足しログ記録、continue により次へ |
継続可能なエラーの局所処理 |
外側の try
|
Exception を捕捉しログ出力後 raise
|
致命的なエラーの伝搬と責任移譲 |
main() |
プログラムエントリーポイントで全体例外を受け止める | ログ記録とプログラム停止の統括処理 |
ネストされた try–except
を使用することで、「その場で処理すべきエラー」と「外部に委譲すべきエラー」を明確に分離できる。この設計は、複数階層の処理が存在する現実のアプリケーション・ETL処理・通信フレームワークなどで非常に重要なパターンである。特に、ログによる可視化とエラーハンドリングの責任の明確化は、堅牢でデバッグ可能なシステムの基盤である。よって、例外処理においては常に「継続すべきか?停止すべきか?通知すべきか?」という設計の意図を明示することが求められる。
Q.13-3
.remove()
で対象要素が複数回存在したとき、1度削除後に再度削除を試みて ValueError
を発生させ、それを補足・再処理する構造を設計せよ。
問題背景
.remove()
の使用と挙動
list.remove(value)
は、リスト中の最初の一致する要素を削除するメソッドである。対象が存在しない場合には、ValueError
が発生する。同じ要素が複数ある場合でも、最初の1つだけが削除される。
lst = [1, 2, 3, 2]
lst.remove(2)
print(lst) # → [1, 3, 2]
【2】ValueError
の補足と処理継続
.remove()
実行時に、指定要素がすでにすべて削除された状態で再実行すると ValueError
が発生する。この例外を try–except
によって補足し、例外の内容をログに記録したり、終了処理へ移行する構造が望ましい。while
ループと組み合わせることで、要素がすべて削除されるまで繰り返す処理も可能である。
【3】Pythonicな責任分離設計の意義
.remove()
の例外処理により、要素が残っているかを try–except
によって動的に判定できる。in
を使って事前チェックする方法もあるが、例外主導で書いた方が明示的なエラー処理となり、操作の責任が明確になる。
# 【1】ログ出力のため logging モジュールを読み込む
import logging
# 【2】ログ出力設定(INFO レベル以上を記録)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】対象リストを定義(同じ値を複数含む)
data = ["apple", "banana", "apple", "cherry", "apple"]
# 【4】削除対象とする要素を指定
target = "apple"
# 【5】削除回数をカウントする変数
removed_count = 0
# 【6】すべての対象を削除するループを開始
while True:
try:
# 【7】対象要素をリストから削除(最初の1つ)
data.remove(target)
removed_count += 1 # 【8】削除成功時にカウントを増加
logging.info(f"{target} を削除しました。現在のリスト: {data}")
except ValueError as e:
# 【9】削除対象が存在しないときに補足される
logging.warning(f"{target} の削除に失敗しました(存在しません)。")
logging.info(f"{removed_count} 件削除されました。最終リスト: {data}")
break # 【10】すべて削除済みなのでループ終了
2025-05-01 20:30:01,123 - INFO - apple を削除しました。現在のリスト: ['banana', 'apple', 'cherry', 'apple']
2025-05-01 20:30:01,124 - INFO - apple を削除しました。現在のリスト: ['banana', 'cherry', 'apple']
2025-05-01 20:30:01,125 - INFO - apple を削除しました。現在のリスト: ['banana', 'cherry']
2025-05-01 20:30:01,126 - WARNING - apple の削除に失敗しました(存在しません)。
2025-05-01 20:30:01,127 - INFO - 3 件削除されました。最終リスト: ['banana', 'cherry']
本問のまとめは以下。
.remove()
による要素削除は、対象が存在しない場合に ValueError
を発生させるため、例外を適切に補足して処理フローを明確に設計する必要がある。本問における try–except
を用いた構造は、複数の削除処理・再処理・エラー記録・継続制御を1つにまとめる実践的でPythonicな手法であり、ログとカウント変数の併用により、処理の透明性と保守性が高められている。
Q.13-4
try-except-else-finally
の各節の実行順序を図示したうえで、else
節が果たす設計的役割と、finally
節との厳密な違いについて述べよ。
問題背景
【1】 try-except-else-finally
の各節の目的
節 | 意味・意図 |
---|---|
try |
例外が発生するかもしれないコードを含む |
except |
try 内で例外が発生した場合にそのエラーを補足し、処理を継続させる |
else |
try 内で例外が発生しなかった場合のみ実行される補助処理 |
finally |
例外の有無にかかわらず常に最後に実行される節(リソースの解放など) |
また、以下のフローにて実行される
+------------------+
| try 実行 |
+------------------+
|
+--------+--------+
| |
例外発生 例外発生せず
| |
+-----------+ +-----------+
| except 節 | | else 節 |
+-----------+ +-----------+
\ /
+---------------+
| finally 節 |
+---------------+
【2】else
と finally
の設計的な違い
観点 |
else 節 |
finally 節 |
---|---|---|
実行条件 |
try 内で例外が発生しなかったときのみ実行される |
常に実行される(例外があってもなくても) |
主な用途 | 安全な後処理(データ保存・ログ出力など) | リソース解放・後始末(ファイルや接続のクローズなど) |
設計哲学 | 明示的に「成功時の追加処理」であることを表現 | 「終了時に必ず行う処理」のための明確な場所を提供 |
解答例とコードによる実践
# 【1】ログ出力のため logging モジュールを読み込む
import logging
# 【2】ログ設定(INFO 以上)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】例外を発生させるか選択できる関数を定義する
def run_division(a, b):
# 【4】関数の開始ログを出力
logging.info("関数を開始しました。")
# 【5】try-except-else-finally 構造の開始
try:
# 【6】割り算を試みる(b が 0 のとき例外が出る)
result = a / b
logging.info("割り算が成功しました。")
except ZeroDivisionError as e:
# 【7】0除算例外が発生した場合に実行される
logging.error(f"ZeroDivisionError: {e}")
else:
# 【8】try 節が例外なく成功したときのみ実行される
logging.info(f"割り算の結果: {result}")
finally:
# 【9】必ず最後に実行される後処理
logging.info("関数を終了します(finally 節)")
# 【10】正常な割り算(例外なし)
print("\n--- 正常系 ---")
run_division(10, 2)
# 【11】異常な割り算(例外あり)
print("\n--- 異常系 ---")
run_division(10, 0)
--- 正常系 ---
2025-05-01 20:55:01,123 - INFO - 関数を開始しました。
2025-05-01 20:55:01,124 - INFO - 割り算が成功しました。
2025-05-01 20:55:01,125 - INFO - 割り算の結果: 5.0
2025-05-01 20:55:01,126 - INFO - 関数を終了します(finally 節)
--- 異常系 ---
2025-05-01 20:55:01,127 - INFO - 関数を開始しました。
2025-05-01 20:55:01,128 - ERROR - ZeroDivisionError: division by zero
2025-05-01 20:55:01,129 - INFO - 関数を終了します(finally 節)
else
節は、処理成功時にだけ実行すべきコードを安全に切り分ける構造であり、成功・失敗によって明確に処理を分岐できる。finally
は、例外の有無に関わらず常に実行される構造であるため、リソース管理や終了メッセージなどに適している。Python の設計原則「明示は黙示に勝る(Explicit is better than implicit)」に則った記述が可能となる。
本問のまとめは以下。
try-except-else-finally
構造をすべて使用することで、Python における例外処理の流れを精密に制御し、可読性・信頼性の高いコードを設計することができる。else
節は「例外が起きなかったときの後処理の安全な実行場所」として機能し、finally
節は「例外の有無にかかわらず必ず行いたい処理」を記述するための場所である。この両者の厳密な役割分担により、堅牢なエラーハンドリング構造が実現される。
Q.13-5
try-except-else
構造を用いて、ある操作が失敗しなかった場合にだけログを記録する場合と、try
内で記録する場合とで、処理の明示性にどのような違いが生まれるかを述べよ。また、else
節の利用が保守性の向上に寄与する例として、「正常系処理をまとめて記述することで、例外処理との分離が実現される」ことについて、実務的観点から論ぜよ。
問題背景
【1】try-except-else
構文の目的
try
節:例外が発生する可能性のあるメイン処理を記述する。
except
節:try
節で例外が発生したときの補足/代替処理を記述する。
else
節:try
節が成功した(=例外が発生しなかった)ときの正常系の後処理を明示的に記述する。
【2】 なぜ「後続処理の分岐」が重要なのか
成功と失敗を明確に構文で分岐することにより、次の処理の責任の所在を明らかにすることができる。ログ記録、結果の保存、ユーザー通知など、結果に応じた適切な処理設計が可能になる。
【3】 正常系と異常系の分離がなぜ重要か(実務的観点)
保守性・可読性の高いコードは「正常系と異常系の責任を構造的に明確に分けて書く」ことが基本原則である。
正常系処理(本来意図したフロー)を else に分離することで、例外処理との混在を避け、拡張・修正が局所的に済む設計が実現できる。
これは テスト容易性、レビュー効率、バグ混入リスクの抑制といった実務上の大きなメリットにつながる。
解答例とコードによる実践
【A】 例外に応じて分岐処理を変える設計
# 【1】ログ出力のため logging をインポート
import logging
# 【2】ログ出力設定(INFOレベル以上)
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】例外の有無に応じて後処理を分ける関数を定義
def validate_input_and_process(value):
# 【4】結果の記録用
result = None
# 【5】try-except-else構造を使って分岐設計
try:
# 【6】入力値が整数かどうかをチェック(エラーの可能性あり)
num = int(value) # 入力が整数でなければ ValueError
except ValueError:
# 【7】例外が発生した場合(整数に変換できない場合)
logging.warning("無効な入力値(整数変換に失敗)")
result = "入力エラー"
else:
# 【8】変換が成功した場合の後続処理
logging.info(f"整数変換成功:{num}")
result = num * 10 # 正常な場合の処理
# 【9】分岐結果に基づく後続処理
return result
# 実行例
print("\n--- 正常入力: '5' ---")
print(validate_input_and_process("5"))
print("\n--- 異常入力: 'five' ---")
print(validate_input_and_process("five"))
--- 正常入力: '5' ---
2025-05-01 21:15:01,123 - INFO - 整数変換成功:5
50
--- 異常入力: 'five' ---
2025-05-01 21:15:01,124 - WARNING - 無効な入力値(整数変換に失敗)
入力エラー
上記の設計的利点は以下。
① 成功処理と失敗処理を構文レベルで明示的に分離できる
else
節を使用することで、成功時の処理が try
節と分離され、例外処理の影響を受けない安全な空間に書ける。読者にとって、「これは失敗しなかったから実行している処理だ」と一目で分かる設計になる。
② 処理責任を状況別に明確に設計できる
except
におけるログ出力や代替返却、else
における通常処理といったように、責任の切り分けが容易である。ロジックの意図や処理の状態が「成功」「失敗」として分類可能となる。
③ 保守性・拡張性に優れる構造である
後から正常系(else
)または異常系(except
)に対して処理を追加する際の影響範囲が限定されている。例外が追加された場合でも、それぞれの責任範囲が明確なため、修正が局所的に済む。
####【B】 ファイル操作における設計例
# 【1】ログ出力モジュールを読み込む
import logging
# 【2】ログ出力の設定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】ファイル読み込み処理の関数定義
def read_config_file(path):
# 【4】try-except-else構造によって処理を構造的に分離する
try:
# 【5】ファイルを読み取りモードで開こうと試みる(例外の可能性あり)
with open(path, 'r') as f:
lines = f.readlines() # 読み取り処理
except FileNotFoundError as e:
# 【6】ファイルが存在しない場合にログ出力(異常系)
logging.error(f"ファイルが見つかりません: {e}")
return None
else:
# 【7】ファイル読み込み成功時のみ行う正常系処理(例:構文チェック・初期化など)
logging.info(f"{path} の読み込みに成功しました。行数: {len(lines)}")
return lines
# 実行例
print("\n--- 存在するファイルを読み込む例 ---")
read_config_file("example.conf")
print("\n--- 存在しないファイルを読み込む例 ---")
read_config_file("notfound.conf")
--- 存在するファイルを読み込む例 ---
2025-05-01 21:20:01,123 - INFO - example.conf の読み込みに成功しました。行数: 42
--- 存在しないファイルを読み込む例 ---
2025-05-01 21:20:01,124 - ERROR - ファイルが見つかりません: [Errno 2] No such file or directory: 'notfound.conf'
上記の設計的利点のまとめ
① 正常系と異常系が構文的に明確に分離される
else
に正常系処理をまとめることで、処理の「成功」を前提としたロジックが明示される。例外が発生しないことが保証された後に行う処理であることが、構文から一目瞭然である。
② 保守性・拡張性が高くなる
正常系と異常系の処理が混在していないため、どこを修正すべきかが明確であり、変更が局所化される。try
や except
に手を加えず、else
節だけで処理を拡張することが容易となる。
③ テストとレビューが効率的に行える
成功パスの処理と失敗パスの処理が明確に分かれていることで、テストケースの設計が容易になる。レビュー時にも、期待される処理の構造が把握しやすく、意図の誤解やバグ混入が防止できる。
本問のまとめは以下。
関数内で try-except-else
構造を使い、例外の発生状況に応じて後続処理を分岐させることで、成功/失敗に応じた明示的な設計・責任の分離・保守性の確保が可能となる。これは、堅牢で可読性の高い例外処理設計を支える重要な構文的手法である。特に業務アプリケーションにおいては、エラーの発生有無に基づくフロー制御を安全かつ明快に記述するための基盤となる。
Q.13-6
try
節で変数を定義し、それを else
節で利用する際に起こり得るスコープの問題について論ぜよ。また、else
節を利用して分離した処理が、モジュール分割や関数分割とどのように対応・補完し合うかについて論ぜよ。
問題背景
【1】try-except-else
におけるスコープの原則
Python の try
, except
, else
はブロック構文ではあるが、新たなスコープを作らない(関数・クラスのようなローカルスコープは形成しない)。したがって、try
節内で定義された変数は、同じ関数内であれば else
節や finally
節からもアクセス可能である。
しかし――
try
節の実行中に例外が発生した場合、変数が定義されないまま else
に進まないことがある。このとき else
節内でその変数にアクセスすると UnboundLocalError
が発生する。
【2】 設計観点:else
とモジュール/関数分離の補完関係
else
節は「例外が発生しなかった場合の後処理」という明確な文法構造を与える。これにより、正常系のロジックを別関数・別モジュールに切り出すための安全な前提条件を与えることができる。モジュール設計においては、「try
でリスクのある処理」「else
で後段の安全な処理」といった責任の分離が可能となる。
【3】 スコープの問題点と回避策
例えば、以下のようなコードでは問題が発生する。
try:
result = int("invalid") # ← ここで ValueError が発生
except ValueError:
pass
else:
print(result) # ← result が定義されていないので UnboundLocalError
解決策:
try
節より前に result = None
のように初期化しておく。変数が try
節で定義されたことを前提にしない else
節の設計が必要である。
設計的補完関係:else
と関数・モジュール分離
要素 | 内容 |
---|---|
else 節 |
try が成功した場合の「安全な後続処理ブロック」として機能する |
関数分離 |
else 内のロジックを別関数に切り出すことで再利用性と単一責任原則を実現 |
モジュール分割 |
else 節から正常系モジュールや処理クラスを呼び出す前提が保証される |
メリットは以下:
- 責任の明確化:異常処理と正常処理の担当範囲を切り分けやすい。
- 再利用性の確保:正常系処理を共通関数に切り出し、テストや他の処理でも流用できる。
- スケーラビリティ:モジュール単位の拡張が安全に行える(
try
→ 成功 → 安全なAPI呼び出しという流れ)。
解答例とコードによる実践
# 【1】ログ出力モジュールの読み込み
import logging
# 【2】ログ設定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】正常系処理を分離した関数
def process_result(value):
# 【4】正常時の処理をここで担当
logging.info(f"正常系関数:受け取った値は {value} です")
# 【5】入力値に応じて処理を分ける関数を定義
def safe_parse_and_process(text):
# 【6】try-except-else構造を用いて処理を分離
try:
# 【7】整数に変換する(失敗する可能性がある)
num = int(text)
except ValueError as e:
# 【8】変換に失敗した場合の処理
logging.error(f"変換失敗: {e}")
return
else:
# 【9】成功した場合のみ、変数 num を使用して関数へ渡す
process_result(num)
# 実行例
print("\n--- 成功ケース:入力 '42' ---")
safe_parse_and_process("42")
print("\n--- 失敗ケース:入力 'forty-two' ---")
safe_parse_and_process("forty-two")
--- 成功ケース:入力 '42' ---
2025-05-01 21:30:01,123 - INFO - 正常系関数:受け取った値は 42 です
--- 失敗ケース:入力 'forty-two' ---
2025-05-01 21:30:01,124 - ERROR - 変換失敗: invalid literal for int() with base 10: 'forty-two'
Q.13-7
try-except-else
構造における、lambda
式や内包表記との組み合わせ時における制約と注意点について論ぜよ。
問題背景
【1】lambda
式・内包表記との併用における制約
要素 | 制約の内容 |
---|---|
lambda 式 |
try –except を構文的に直接含むことはできない(構文エラーになる) |
内包表記 | 内包表記の中ではtry –except を**if のように直接は記述できない** |
回避策 |
lambda 内では try-except を使わず、別関数に委譲する。内包表記では 外部関数で例外処理を行う設計が必要である。 |
【2】lambda
式の組み合わせの制限と回避
使用できない例(構文エラー)
# 構文エラーとなるlambda 式(tryを含められない)
f = lambda x: try: int(x) except: None ← NG
回避方法(関数委譲)
# lambda ではなく安全な補助関数を定義し、内包表記等から呼び出す
f = lambda x: safe_to_int(x)
【3】設計上の注意と補足
-
lambda 式は1式のみしか書けない構文である
try-except
やif
文のような文(statement)は記述不可である。そのため、例外処理を含むラムダ定義は不可能であり、関数化が必要となる。 -
内包表記内の例外処理は冗長・複雑になりやすいため関数抽象化によって明示性を担保すべきである
例外処理ロジックが長くなると内包表記の可読性が大幅に低下する。safe_
関数のように設計することで、失敗時の処理も明示的に分離され、ログ記録や分岐が行いやすくなる。
解答例とコードによる実践
# 【1】ログ出力モジュールを読み込む
import logging
# 【2】ログ出力の設定
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
# 【3】例外を補足しつつ int 変換する補助関数を定義
def safe_to_int(s):
# 【4】try-except-else 構造で変換と成功処理を分離
try:
value = int(s) # 【5】int変換(例外の可能性あり)
except ValueError:
logging.warning(f"変換失敗: {s!r}") # 【6】失敗ログ
return None
else:
return value # 【7】成功時のみ返す
# 【8】変換対象のリストを定義
data = ["1", "2", "a", "3", "b", "4"]
# 【9】内包表記で安全に変換された数値のみを抽出(None除外)
# 【10】safe_to_int を使用して、構文的に安全な変換を実現
numbers = [n for n in (safe_to_int(x) for x in data) if n is not None]
# 【11】結果出力
print("変換成功リスト:", numbers)
2025-05-01 21:45:01,123 - WARNING - 変換失敗: 'a'
2025-05-01 21:45:01,124 - WARNING - 変換失敗: 'b'
変換成功リスト: [1, 2, 3, 4]
本問のまとめは以下
try-except-else
構造は、明示的に成功処理・失敗処理を分離するための優れた構文であるが、ラムダ式や内包表記の中で直接使用することはできないという構文上の制限が存在する。そのため、例外処理が必要な処理は関数として切り出し、呼び出し元で明示的に制御する設計が望ましい。
これは、関数の抽象化、責任の明確化、ログ記録の明示性、テストの容易性など、あらゆる設計的観点において有効であり、Python の構文制約に対応しつつ、Pythonic な設計原則に適った構成である。
あとがき
今回は「例外処理」について扱いました。個人的にはもっと扱いたかった問題も多いのですが、盲点となりがちな話や重要そうに感じる話題を中心に選んでみました。次回も「例外処理」について扱う予定です。
参考文献
[1] 独習Python (2020, 山田祥寛, 翔泳社)
[2] Pythonクイックリファレンス 第4版(2024, Alex, O’Reilly Japan)
[3] 【Python 猛特訓】100本ノックで基礎力を向上させよう!プログラミング初心者向けの厳選100問を出題!(Youtube, https://youtu.be/v5lpFzSwKbc?si=PEtaPNdD1TNHhnAG)