Skip to content
This page has been auto-translated and may contain errors.View in English

ファイルと例外

実用的な作業を行うプログラムのほとんどは、ファイルシステムに触れます。設定の読み込み、結果の書き出し、データの読み込みなどです。そして問題が起きると、Pythonは例外を発生させます。例外とは、予期しないことが起きたというシグナルです。この章では、その両方を扱います。ファイルからのデータの入出力と、クラッシュではなく適切にエラーを処理するコードの書き方です。

ファイルI/Oと例外処理は、プログラムを堅牢にする2つのメカニズムです。open()はファイルシステムへのアクセスを提供し、withはエラー時にもクリーンアップが行われることを保証します。try/exceptは特定の例外型をキャッチし、クラッシュする代わりに適切に回復できるようにします。これらを組み合わせれば、無人で動作するスクリプトの基盤になります。

Pythonにおけるファイル操作は、OSのファイルディスクリプタ抽象化を介して行われます。open()はファイルオブジェクトを返し、その型はモードとバッファリングの設定によって異なります。コンテキストマネージャは__enter____exit__によってリソースのクリーンアップを保証します。例外処理は構造化されたアンワインドを使用します。例外が伝播するとき、Pythonはコールスタックを遡って一致するexcept句を探し、その途中でfinallyブロックを必ず実行します。

ファイルを開く

open()はファイルを開き、読み書きできるオブジェクトを返します。パスと、ファイルに対して何をしたいか(読み込み、書き込み、追記)を指定します。使い終わったら必ずファイルを閉じてください。with文を使えば、これを自動的に行ってくれます。

open(path, mode)はファイルオブジェクトを返します。モード文字列はアクセス方法を制御します。"r"は読み込み、"w"は書き込み(最初に切り詰め)、"a"は追記です。"b"を加えるとバイナリモードになります。テキストモードのデフォルトエンコーディングはシステムロケールです。可搬性のためには明示的にencoding="utf-8"を指定してください。

open()はOSを呼び出してファイルディスクリプタを割り当て、バッファ付きI/Oラッパーを返します。テキストモードではTextIOWrapperでラップされ、エンコーディングとユニバーサル改行を扱います。バイナリモードではBufferedReaderまたはBufferedWriterが返されます。encodingパラメータは可搬性のために重要です。デフォルト(locale.getpreferredencoding())はプラットフォームによって異なります。特別な理由がない限り、テキストファイルには常にencoding="utf-8"を指定してください。

python
f = open("data.txt", "r")    # "r" = 読み込み
content = f.read()
f.close()

"r"モードです:

モード意味
"r"読み込み。ファイルが存在している必要があります。デフォルトモード。
"w"書き込み。ファイルを作成または上書きします。
"a"追記。消去せず末尾に追加します。
"x"作成。ファイルが既に存在する場合は失敗します。
"r+"読み書き両用。
"b"バイナリ。任意のモードに追加: "rb", "wb"

使い終わったら必ず.close()を呼んでください。忘れるとファイルがロックされたままになり、データの破損につながる可能性があります。これを確実に処理する方法がwith文です。

with

with open(...)はファイルを管理してくれて、インデントされたブロックが終わるとき、たとえエラーが起きた場合でも自動的にファイルを閉じます。手動でopen()/close()を使うのではなく、常にwith open(...)を使ってください。より安全で、これが標準です。

withはPythonのコンテキストマネージャ構文です。開始時に__enter__を呼び、終了時(例外が発生した場合でも)に__exit__を呼びます。ファイルの場合、__exit__はファイルディスクリプタを閉じます。これにより、すべてのファイルアクセスにtry/finallyを巻き付けなくてもクリーンアップが保証されます。

with expr as nameexpr.__enter__()を呼び、その結果をnameにバインドします。終了時(通常または例外)にはexpr.__exit__(exc_type, exc_val, tb)が呼ばれます。__exit__が真値を返すと、例外は抑制されます。ファイルオブジェクトはこのプロトコルを実装しています。__exit__self.close()を呼びます。コンテキストマネージャは積み重ねることができます。with open(a) as f, open(b) as g:は複数ファイルを扱う際の慣用的な書き方です。

python
with open("data.txt", "r") as f:
    content = f.read()

# ここで f は確実に閉じられる

withは何をするのか?

withはPythonのコンテキストマネージャ構文です。セットアップとティアダウンのコードを代わりに実行します。この場合、ファイルを開き、確実に閉じます。内部の仕組みを知る必要はありません。open()と一緒に使えばよいだけです。

ファイルを読む

読み込みには3つの方法があります。.read()はファイル全体を1つの文字列としてロードします。.readline()は1行ずつ読み込みます。ファイルオブジェクト自体を直接イテレートすると、行ごとに読み込みます。これは大きなファイルでは最も効率的な方法です。すべてを一度にメモリにロードしないからです。

.read()はファイル全体をメモリにロードします。.readline()は改行文字を含む1行を読みます。.readlines()はすべての行のリストを返します。ファイルオブジェクトを直接イテレートするのは、大きなファイルに対して最もメモリ効率の良いパターンです。バッファから1行ずつ読み込み、全コンテンツを保持しません。

.read()はEOFまで読み込み、内容を文字列(バイナリモードではバイト列)として返します。.readline()\nまたはEOFまで読み込みます。ファイルオブジェクトをイテレートすると__iter__が呼ばれ、それがreadline()を繰り返し呼びます。O(1)のメモリ消費で行ごとに処理できます。.readlines()list(file)と同等ですが、すべての行を事前にマテリアライズします。非常に大きなファイルには、.read()よりもイテレータパターンが推奨されます。

python
with open("data.txt", "r") as f:
    content = f.read()          # ファイル全体を1つの文字列として

with open("data.txt", "r") as f:
    first_line = f.readline()   # 1行ずつ

with open("data.txt", "r") as f:
    lines = f.readlines()       # 行のリスト、各行は "\n" で終わる

大きなファイルの場合、1行ずつ読む方が一度に全部ロードするより効率的です:

python
with open("big_file.txt", "r") as f:
    for line in f:              # ファイルを直接イテレート、メモリ効率が良い
        print(line.strip())     # strip() は末尾の改行を削除

ファイルオブジェクトを直接イテレートする(for line in f)のは、大きなファイルを読むのに最も効率的で慣用的な方法です。

ファイルに書き込む

"w"モードはファイルが存在する場合、その内容を完全に上書きします。"a"モードは末尾に追加します。.write()は改行を自動的に追加しません。各行の末尾に明示的に"\n"を含めてください。複数行を一度に書き込むには、"\n".join()で結合します。

.write(s)は文字列を書き込み、書き込まれた文字数を返します。改行は追加しません。.writelines(iterable)はイテラブルからの各文字列を区切りなしで書き込みます。"w"は開く時に切り詰めます。"a"はEOFにシークします。可搬性のためには、プラットフォーム固有の改行をハードコーディングするより、"\n"またはos.linesepを使用してください。

.write()はバッファに書き込みます。.flush()または.close()が呼ばれるまでデータはディスクに到達しない可能性があります。"w"モードは開く時にtruncate(0)を呼び、以前の内容を破壊します。"a"モードは各書き込み前にEOFへシークするため、ほとんどのOSで並行追記が安全に行えます。f.writelines()は各項目に対してf.write()を呼びます。追加のメモリ割り当てはありませんが、区切り文字も追加されません。

python
with open("output.txt", "w") as f:
    f.write("Hello, world\n")

with open("output.txt", "a") as f:
    f.write("Another line\n")

"w"はファイルが存在する場合に完全に上書きします。"a"は末尾に追加します。

f.write()は改行を自動的に追加しないので、明示的に"\n"を含めてください。複数行を一度に書き込むには:

python
lines = ["Line one", "Line two", "Line three"]

with open("output.txt", "w") as f:
    f.write("\n".join(lines) + "\n")

例外

Pythonが処理できない問題に遭遇すると、例外を発生させます。これは何が起きたかとどこで起きたかを記述するエラーです。処理しなければ、プログラムはクラッシュし、トレースバックが表示されます。下の表は、よく遭遇する例外を示しています。

例外はBaseExceptionを継承するオブジェクトです。ユーザー向けの例外はすべてExceptionを継承します。発生すると、Pythonはコールスタックを遡って一致するexcept句を探します。トレースバックは例外発生地点からエントリポイントまでのフルコールチェーンを示します。

例外オブジェクトは型、メッセージ、およびトレースバックオブジェクトを指す__traceback__属性を持ちます。raiseは新しい例外を作成します。引数なしのraiseは、元のトレースバックを保持しながら現在の例外を再発生させます。例外チェーン(raise B from A)は2つの例外をリンクし、トレースバックに表示されます。BaseExceptionがルートです。KeyboardInterruptSystemExitExceptionではなく直接これを継承しているため、except Exception単独ではキャッチできません。

よく遭遇する例外:

例外発生する状況
FileNotFoundErroropen()がファイルを見つけられない
ValueError関数が正しい型だが内容が間違った値を受け取った、例: int("abc")
TypeError完全に型が間違っている、例: "hello" + 5
KeyError辞書のキーが存在しない
IndexErrorリストのインデックスが範囲外
ZeroDivisionErrorゼロ除算
AttributeErrorオブジェクトがその属性やメソッドを持たない

try / except

失敗する可能性のあるコードをtryブロックでラップしてください。例外が発生すると、対応するexceptブロックがそれを処理し、クラッシュを防ぎます。どの例外をキャッチするかは具体的に指定してください。except:だけですべてをキャッチすると、本当のバグが隠れてしまいます。

try/exceptは特定の例外型をインターセプトします。無関係なエラーを静かに飲み込まないように、型を指定してください。as eで例外を名前にバインドし、メッセージを調べることができます。複数のexcept句で異なる例外型を処理できます。Pythonは最初に一致する句を選びます。

except ExceptionType as eisinstance(raised_exception, ExceptionType)が真の場合にマッチします。つまり、Exceptionをキャッチすると、そのすべてのサブクラスもキャッチします。Pythonは型がマッチする最初のexcept句にマッチし、後続の句はスキップされます。except (A, B) as eはいずれかの型をキャッチします。except:単独ではKeyboardInterruptSystemExitも含めてすべてキャッチしてしまい、ほとんど意味がありません。

python
try:
    value = int("abc")
except ValueError:
    print("That's not a valid number")

どの例外をキャッチするかは具体的に指定してください。except:だけですべての例外をキャッチするとバグが隠れます:

python
# 悪い例、プログラマのミスも含めてすべてキャッチする
try:
    result = do_something()
except:
    pass

# 良い例、期待していて実際に処理できるものだけをキャッチする
try:
    result = do_something()
except FileNotFoundError:
    print("File not found")

複数の例外をキャッチする

異なるエラー型を別々のexceptブロックで処理することも、タプルを使って1つのブロックで複数の型をキャッチすることもできます。as eの部分でエラーメッセージにアクセスできます。

複数のexcept句は上から下に評価され、最初にマッチしたものが勝ちます。タプルで複数の型をキャッチするとそのいずれかをキャッチします。as eを使うと例外インスタンスがバインドされ、メッセージ、型、トレースバックにアクセスできます。最も具体的な型を先に、より一般的な型を後に書くことでシャドウイングを避けられます。

except (A, B) as eは両方をキャッチする単一句の糖衣構文です。as eによるバインディングはexceptブロックを抜けると(トレースバックとの参照サイクルを防ぐために)クリアされます。追加のコンテキストとともに再発生させるには、raise ValueError("context") from eを使います。これは__cause__を設定し、トレースバックに両方の例外を表示します。

python
try:
    data = int(user_input)
    result = 100 / data
except ValueError:
    print("Not a number")
except ZeroDivisionError:
    print("Can't divide by zero")

または、タプルで複数をキャッチします:

python
except (ValueError, ZeroDivisionError) as e:
    print(f"Input error: {e}")

as eは例外オブジェクトを名前にバインドし、メッセージを調べられるようにします。

elseとfinally

elseは例外が発生しなかった場合にのみ実行されます。finallyは例外があってもなくても常に実行されます。finallyは、何があっても実行する必要があるクリーンアップに有用です。

elseは「通常のパス」のコードをtry本体から分離し、else内のコードは例外が発生しなかった場合にのみ実行されることを明確にします。finallyはクリーンアップの保証です。例外が発生、キャッチ、または再発生されても実行されます。returnbreakに遭遇しても実行されます。

elsetryブロックが例外を発生させずに完了した場合に実行されます。finallyreturnbreakcontinue、未処理例外の後でも無条件に実行されます。finallyと呼び出しコードの両方にreturn文がある場合、finallyreturnが優先されます。ファイルハンドルやデータベース接続を扱うfinallyは、withが使えない場合の二次的なセーフティネットです。

python
try:
    with open("data.txt") as f:
        content = f.read()
except FileNotFoundError:
    print("File not found, using defaults")
    content = ""
else:
    print("File loaded successfully")
finally:
    print("Done attempting to load file")   # 常に実行される

finallyは、ファイルに対して既にwithを使用していても、接続を閉じる、ロックを解放するなどのクリーンアップに最も有用です。

raise

raiseを使って自分で例外を発生させることもできます。これにより、関数が間違った値を静かに返すのではなく、呼び出し側に問題を明確に伝えることができます。

raise ExceptionType("message")は例外を作成して発生させます。exceptブロック内で引数なしのraiseを使うと、現在の例外を再発生させます。関数から例外を発生させると、エラー条件が明示的になり、呼び出し側がそれを特定して処理できるようになります。

raise exprexprを評価して例外インスタンスを取得し、__traceback__を設定します。raise単独はトレースバックを変更せずに現在の例外を再発生させます。raise B from AB.__cause__ = A(明示的なチェーン)を設定します。raise B from Noneはコンテキスト表示を抑制します。キャッチしてログを取った後、元のトレースバックを保持しながら伝播させるにはraiseが最もクリーンな方法です。

python
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

これにより関数が何を期待しているかが明示され、呼び出し側に問題が明確に伝わります。

カスタム例外クラス

大規模なプログラムでは、Exceptionを継承して独自の例外型を定義できます。これにより、呼び出し側はあなた固有のエラーを他の種類のエラーとは別にキャッチできます。

カスタム例外は、呼び出し側が適切な具体性のレベルでキャッチできる階層を作ります。Exceptionをサブクラス化するだけで通常は十分です。例外ファミリーの場合、ベース例外クラスを作成し、特定のエラーモードに対してサブクラス化します。すると呼び出し側はベース型をキャッチしてすべてのバリアントを処理できます。

カスタム例外は(BaseExceptionではなく)Exceptionをサブクラス化すべきです。ドメイン固有のフィールドを持つ__init__を追加することで、呼び出し側はメッセージ文字列を読むだけでなく、プログラム的に例外を調べられます。例外階層により呼び出し側は具体性のレベルを選択できます。except PaymentErrorは支払い関連のすべてのエラーをキャッチし、except InsufficientFundsErrorはその特定のケースのみをキャッチします。

python
class InsufficientFundsError(Exception):
    pass

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(
                f"Cannot withdraw {amount}, balance is {self.balance}"
            )
        self.balance -= amount
python
try:
    account.withdraw(1000)
except InsufficientFundsError as e:
    print(f"Transaction declined: {e}")

JSON

JSONはあらゆるものが扱う形式です。API、設定ファイル、データのエクスポートなど。Pythonのjsonモジュールがこれを直接処理します。json.load()はファイルからJSONを読み込んでPythonの辞書やリストにします。json.dump()はPythonの辞書やリストをJSONとしてファイルに書き込みます。

json.load()json.dump()はファイルオブジェクトを扱います。json.loads()json.dumps()は文字列を扱います。dump/dumpsindent=パラメータは出力を人間が読みやすくします。json.JSONDecodeError(ValueErrorのサブクラス)は無効なJSONで発生します。

json.load()はストリーミングパーサです。ファイルオブジェクトから段階的に読み込みます。json.dumps()は文字列を返します。json.dump().write()をサポートするファイルライクオブジェクトに書き込みます。dump/dumpsdefault=パラメータはJSONシリアライズできない型を処理します。シリアライズできないオブジェクトを受け取り、シリアライズ可能なものを返す必要があります。load/loadsobject_hookはパースされた各オブジェクト辞書をインターセプトし、カスタムデシリアライゼーションを可能にします。

ファイルからJSONを読む:

python
import json

with open("config.json", "r") as f:
    config = json.load(f)    # JSON を Python の dict/list にパース

print(config["setting"])

JSONをファイルに書く:

python
import json

data = {"name": "さくら", "score": 87, "active": True}

with open("output.json", "w") as f:
    json.dump(data, f, indent=2)    # indent= で人間が読みやすくなる

JSONからPythonへの型のマッピング:

JSONPython
object {}dict
array []list
string ""str
numberint または float
true / falseTrue / False
nullNone

ファイルを介さずにJSON文字列とPythonオブジェクトを変換するには:

python
import json

# 文字列から Python へ
data = json.loads('{"name": "さくら", "score": 87}')

# Python から文字列へ
text = json.dumps({"name": "さくら", "score": 87}, indent=2)

json.load()はファイルオブジェクトから読みます。json.loads()("s"付き)は文字列から読みます。

実践

簡単なゲームのセーブ/ロードパターン。状態をJSONに書き出し、次回起動時に読み戻し、セーブファイルがまだない場合はデフォルトにフォールバックします:

python
import json

SAVE_FILE = "save_game.json"

def save_game(player_data: dict) -> None:
    with open(SAVE_FILE, "w") as f:
        json.dump(player_data, f, indent=2)
    print("Game saved.")

def load_game() -> dict:
    try:
        with open(SAVE_FILE, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        print("No save file found, starting fresh.")
        return {"name": "Player", "score": 0, "level": 1}

state = load_game()
state["score"] += 50
save_game(state)

設定ファイルを読み込んで結果を保存する例。各失敗モードに対して具体的な例外処理を行います:

python
import json

def load_config(path: str) -> dict:
    try:
        with open(path, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        raise FileNotFoundError(f"Config file not found: {path}")
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON in {path}: {e}")

def save_results(results: list[dict], path: str) -> None:
    with open(path, "w") as f:
        json.dump(results, f, indent=2)
    print(f"Saved {len(results)} result(s) to {path}")

config  = load_config("experiment.json")
results = [{"epoch": 1, "loss": 0.82}, {"epoch": 2, "loss": 0.61}]
save_results(results, "results.json")

タイムスタンプ付きエントリをファイルに追記する構造化ログライター。予期しない失敗をキャッチしてログに記録するトップレベルのハンドラを備えています:

python
import json
from datetime import datetime

LOG_FILE = "run.log"

def log(level: str, message: str) -> None:
    ts    = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    entry = f"[{ts}] [{level.upper():7}] {message}\n"
    with open(LOG_FILE, "a") as f:
        f.write(entry)

def process(config_path: str) -> None:
    log("info", f"Starting job, config: {config_path}")
    try:
        with open(config_path) as f:
            config = json.load(f)
        log("info", f"Loaded config: {config}")
    except FileNotFoundError:
        log("error", f"Config not found: {config_path}")
        raise
    except json.JSONDecodeError as e:
        log("error", f"Bad JSON in config: {e}")
        raise

try:
    process("config.json")
except Exception as e:
    log("critical", f"Job failed: {e}")

ログ記録後に再発生させることで、呼び出し側のために元のトレースバックが保持されます。トップレベルのexcept Exceptionはすり抜けたものをすべてキャッチし、クリティカルとしてログに記録し、プロセスをきれいに終了させます。