Q1with X() as y: の y に渡される値は、どれの 戻り値 ですか?
with 文とコンテキストマネージャー — __enter__ / __exit__ で安全に開閉する
Python の with 文とコンテキストマネージャーを基礎から解説します。__enter__ と __exit__ のペアで外部リソースを安全に開閉する仕組み、自作コンテキストマネージャーの書き方まで丁寧に学べます。
前回はクラス 内部の属性 をどう守るかを学びました。この記事ではもう一段外側 — Python の外にある 外部リソース(ファイル・データベース接続・ネットワーク・ロックなど)の 取得と解放 を安全に扱う仕組み、with 文と コンテキストマネージャー を整理します。
なぜ with 文が必要なのか
ファイルを開く、データベースに接続する、といった操作には 「使い終わったら必ず閉じる」 後始末が伴います。閉じ忘れるとどうなるか — ファイルディスクリプタは枯渇し、DB コネクションは握ったまま、外部プロセス側もリソースを抱え続けます。
close() を呼ばないと、外部側もずっと「次の指示待ち」のままリソースを保持 し続けてしまう。同じことは try / finally でも書けますが、finally の中で必ず close() を呼ぶ ことを書き手が毎回意識する必要があります。コード量が増え複数人で触ると、書き忘れが発生するかもしれません。
with 文は、取得と解放を 1 セットの構文に閉じ込めて自動化 します。with open("file.txt") as f: がまさにこの仕組みで、with ブロックを抜けた瞬間にファイルが必ず閉じられます。
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 の効能で、開発者は 接続を切り忘れる心配から解放されます。
__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 しない) にして、ログや通知だけ済ませてから上に投げ直すのが安全です。
None、例外発生時 は 「クラス」「例外オブジェクト」「トレースバック」 の 3 点セットが届く。raise ValueError("invalid") のような具体例で見ると中身がイメージしやすい。with の中で例外が起きると、例外オブジェクトの情報が __exit__ の 3 引数に詰めて渡される。__exit__ の戻り値で例外を 握りつぶす(True)か、外へ伝播させる(False)か を選べる。__exit__ で True を返すと例外が消える
__exit__ で True を返すと、with の中で起きた例外は外に伝播しません。便利そうに見えますが、呼び出し側が「処理は成功した」と勘違いする 危険があります。基本は False か 何も return しない にして、ロギングは済ませても 例外は外に上がらせる 方針が安全です。
では実際に、with ブロックの中で例外を発生させてみて、__exit__ の 3 引数に何が届くかを観察してみましょう。
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 を書く必要なし
- 呼び出し側 —
try/finallyを毎回書く - 閉じ忘れ — どこか 1 箇所のコピペミスで発生
- 変更コスト — 後始末の手順が増えると全箇所修正
- 呼び出し側 —
with X() as y:の 1 行のみ - 閉じ忘れ — 構文レベルで起きない(
__exit__は必ず呼ばれる) - 変更コスト — 後始末を増やしたければ
__exit__だけ修正
with の価値。利用箇所が増えても、後始末ロジックの改修は クラス 1 つに閉じる。with はリソースの取得と解放がペアの場所で使う
ファイル / DB 接続 / ロック / ネットワークソケット など、「使い始めるときにリソースを取り、使い終わったら必ず返す」 タイプの操作はすべて with 化を検討します。Python 標準ライブラリにも open() / threading.Lock() / sqlite3.connect() など、最初からコンテキストマネージャーとして使える ものが多数あります。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2with ブロックの中で 例外が発生 したとき、__exit__ の挙動として 正しい ものはどれですか?
Q3__exit__ の 戻り値 が True のとき、with ブロック内で発生した例外はどうなりますか?