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

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

Pythonのwith文とコンテキストマネージャーをDatabaseManagerの自作を通して解説。__enter__/__exit__の役割、exc_type/exc_val/tracebackへの例外伝達、try/finallyとの違いを確認できます。

前回はクラス内部の属性をどう守るかを学びました。この記事ではもう一段外側 — 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ブロック内で発生した例外はどうなりますか?