Q1with X() as y:のyに渡される値は、どれの戻り値ですか?
with 文とコンテキストマネージャー — __enter__ / __exit__ で安全に開閉する
Pythonのwith文とコンテキストマネージャーをDatabaseManagerの自作を通して解説。__enter__/__exit__の役割、exc_type/exc_val/tracebackへの例外伝達、try/finallyとの違いを確認できます。
前回はクラス内部の属性をどう守るかを学びました。この記事ではもう一段外側 — 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ブロック内で発生した例外はどうなりますか?