順番に読み進めながら学べます

with 文とコンテキストマネージャー — __enter__ / __exit__ で安全に開閉する

Python の with 文とコンテキストマネージャーを基礎から解説します。__enter__ と __exit__ のペアで外部リソースを安全に開閉する仕組み、自作コンテキストマネージャーの書き方まで丁寧に学べます。

前回はクラス 内部の属性 をどう守るかを学びました。この記事ではもう一段外側 — Python の外にある 外部リソース(ファイル・データベース接続・ネットワーク・ロックなど)の 取得と解放 を安全に扱う仕組み、with 文と コンテキストマネージャー を整理します。

なぜ with 文が必要なのか

ファイルを開く、データベースに接続する、といった操作には 「使い終わったら必ず閉じる」 後始末が伴います。閉じ忘れるとどうなるか — ファイルディスクリプタは枯渇し、DB コネクションは握ったまま、外部プロセス側もリソースを抱え続けます。

閉じ忘れると「外部プロセス側」もリソースを抱え続ける
Pythonプロセスコネクション張るMySQL / OSリソース確保close() で両方解放 ✅Python(処理終了)コネクション張りっぱなしMySQL / OSリソース抱えたままFD 枯渇 /接続上限超過 ❌
DB やファイルは Python の 外側のプロセス/OS にもリソースを掴ませて使う。Python が close() を呼ばないと、外部側もずっと「次の指示待ち」のままリソースを保持 し続けてしまう。

同じことは try / finally でも書けますが、finally の中で必ず close() を呼ぶ ことを書き手が毎回意識する必要があります。コード量が増え複数人で触ると、書き忘れが発生するかもしれません。

with 文は、取得と解放を 1 セットの構文に閉じ込めて自動化 します。with open("file.txt") as f: がまさにこの仕組みで、with ブロックを抜けた瞬間にファイルが必ず閉じられます。

with X() as y: の実行フロー
with X() as y:突入__enter__呼び出し戻り値がy に入るブロック内で処理__exit__後始末return終了
with に入ると __enter__ が呼ばれ、その戻り値が as で受ける変数に入る。ブロックを抜けるときは正常でも例外でも 必ず __exit__ が呼ばれて後始末が走る。

コンテキストマネージャーを自作する — __enter__と__exit__

with と組み合わせられるオブジェクトを コンテキストマネージャー と呼びます。クラスに 2 つの特殊メソッド を実装するだけで、自作のコンテキストマネージャーになります。

- __enter__(self)with に入った瞬間に呼ばれる。戻り値が as の変数に入る

- __exit__(self, exc_type, exc_val, traceback)with を抜けるときに呼ばれる。正常終了でも例外でも必ず実行される

DB 接続を題材にした最小サンプルが下のコードです(実際の DB ライブラリは使わず、文字列で疑似的に再現します)。

class DatabaseManager:
    def __init__(self, db_name):
        self.db_name    = db_name
        self.connection = None       # まだ接続していない

    def __enter__(self):
        print(f"データベース {self.db_name} に接続")
        self.connection = f"connection_to_{self.db_name}"   # 本物なら接続オブジェクト
        return self.connection                              # as で受け取る値

    def __exit__(self, exc_type, exc_val, traceback):
        print(f"データベース {self.db_name} から切断")
        self.connection = None                              # 後始末
        return False                                        # 例外は握りつぶさない


with DatabaseManager("user_data_db") as conn:
    print(f"  使用中の接続: {conn}")
    print("  データを挿入")
# ↑ ここでブロックを抜けた瞬間に __exit__ が走る

実行すると、出力は 「接続 → 使用中の処理 → 切断」 の順に並びます。ブロックを抜けたら誰が呼ばなくても切断処理が走るのが with の効能で、開発者は 接続を切り忘れる心配から解放されます

上のサンプルと同じ構造で、DatabaseManager クラスを自分で書き、with 経由で動かします。

class DatabaseManager: を定義し、__init__(self, db_name)self.db_name = db_nameself.connection = None を代入してください。

__enter__(self) を定義し、print(f"データベース {self.db_name} に接続") を出力したあと self.connection = f"connection_to_{self.db_name}" を入れて、self.connection を return してください。

__exit__(self, exc_type, exc_val, traceback) を定義し、print(f"データベース {self.db_name} から切断") を出力して、self.connection = None を実行し、最後に return False してください。

with DatabaseManager("user_data_db") as conn: の中で print(f"使用中の接続: {conn}")print("データを挿入") を順に呼んでください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

__exit__ の 3 引数 — 例外も受け取れる

__exit__exc_type / exc_val / traceback という 3 つの引数を取ります。with ブロックの中で 例外が発生したかどうか を、Python が この引数経由で __exit__ に渡してくる のです。

- 正常終了したとき — 3 つとも None。普通に後始末すれば OK

- 例外で抜けたときexc_type は例外クラス、exc_val はそのインスタンス、traceback はトレースバック

さらに __exit__ の戻り値 にも意味があります。True を返すと例外を握りつぶし、ブロック外には伝播しません。False / None を返せば、後始末を済ませた上で例外が外に投げ直されます。原則は False(または何も return しない) にして、ログや通知だけ済ませてから上に投げ直すのが安全です。

__exit__ の 3 引数に届く具体的な値
with の中で起きたことexc_typeexc_valtraceback正常終了(例外なし)NoneNoneNoneraiseValueError("invalid")<class'ValueError'>ValueError('invalid')<tracebackobject>
正常終了 なら 3 つとも None例外発生時「クラス」「例外オブジェクト」「トレースバック」 の 3 点セットが届く。raise ValueError("invalid") のような具体例で見ると中身がイメージしやすい。
正常終了 vs 例外発生時の __exit__
with 内正常終了exc_type= None など__exit__後始末のみwith の外へ抜けるwith 内例外発生exc_type / val/ traceback__exit__ログ + 後始末return False→ 例外伝播
with の中で例外が起きると、例外オブジェクトの情報が __exit__ の 3 引数に詰めて渡される__exit__ の戻り値で例外を 握りつぶす(True)か、外へ伝播させる(False)か を選べる。

__exit__ で True を返すと例外が消える

__exit__True を返すと、with の中で起きた例外は外に伝播しません。便利そうに見えますが、呼び出し側が「処理は成功した」と勘違いする 危険があります。基本は False何も return しない にして、ロギングは済ませても 例外は外に上がらせる 方針が安全です。

では実際に、with ブロックの中で例外を発生させてみて、__exit__ の 3 引数に何が届くかを観察してみましょう。

実践 1 の DatabaseManager を改造して、例外発生時にも __exit__ が必ず走ること と、3 引数に値が渡ってくること を確認します。

class DatabaseManager: を定義し、__init__(self, db_name)self.db_name = db_name を代入してください。

__enter__(self)print(f"接続: {self.db_name}") を出して return self してください(as で manager 自身を受け取る形)。

__exit__(self, exc_type, exc_val, traceback)3 引数を print("exc_type:", exc_type) のように 1 行ずつ出力してから、print(f"切断: {self.db_name}") を出力し、最後に return False してください。

try: ブロックの中で with DatabaseManager("shop_db"): を開き、その中で print("処理開始") のあとに raise ValueError("在庫データが不正") を実行してください。

try: ブロックを except ValueError as e: で受け、print(f"外側でキャッチ: {e}") を出力してください。

Python エディタ

コードを実行してください

try / finally との比較 — なぜwithを選ぶのか

コンテキストマネージャーの仕事は、try / finally でも書けます。それでも with を選ぶ理由は、「リソースの開閉ペア」をクラス側に閉じ込められる からです。同じ仕事を 2 通りで書いてみると、呼び出し側のコード量と読みやすさの差がはっきり見えます。

# ❌ try / finally — 呼ぶ側が後始末コードを毎回手書き
db = DatabaseManager("shop_db")
conn = db.open()                      # 自前の接続メソッドを呼ぶ
try:
    use(conn)                         # 本処理
finally:
    db.close()                        # 閉じ忘れ厳禁。コピペで散らばる


# ✅ with — 開閉のペアはクラス側に閉じ込め、呼ぶ側は本処理だけ
with DatabaseManager("shop_db") as conn:
    use(conn)                         # finally を書く必要なし
with で「開閉の責任」をクラス側に集める
try / finally で書く場合
  • 呼び出し側try / finally を毎回書く
  • 閉じ忘れ — どこか 1 箇所のコピペミスで発生
  • 変更コスト — 後始末の手順が増えると全箇所修正
with 文 + コンテキストマネージャー
  • 呼び出し側with X() as y: の 1 行のみ
  • 閉じ忘れ — 構文レベルで起きない(__exit__ は必ず呼ばれる)
  • 変更コスト — 後始末を増やしたければ __exit__ だけ修正
「リソースを使う側」と「開閉の責任を負う側」を分離 できるのが with の価値。利用箇所が増えても、後始末ロジックの改修は クラス 1 つに閉じる

with はリソースの取得と解放がペアの場所で使う

ファイル / DB 接続 / ロック / ネットワークソケット など、「使い始めるときにリソースを取り、使い終わったら必ず返す」 タイプの操作はすべて with 化を検討します。Python 標準ライブラリにも open() / threading.Lock() / sqlite3.connect() など、最初からコンテキストマネージャーとして使える ものが多数あります。

QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1with X() as y:y に渡される値は、どれの 戻り値 ですか?

Q2with ブロックの中で 例外が発生 したとき、__exit__ の挙動として 正しい ものはどれですか?

Q3__exit__戻り値True のとき、with ブロック内で発生した例外はどうなりますか?