with文は、使い終わったリソースの後始末を自動でやってくれる仕組みです。もっとも身近な例がファイルの読み書きで、with open(...) as f:と書くと、ブロックを抜けるときに自動でファイルが閉じられます。閉じ忘れの心配がなく、エラーが起きても確実に後始末されます。
このwithで使えるオブジェクトをコンテキストマネージャと呼び、__enter__(開始時)と__exit__(終了時)という2つのメソッドで作れます。ポイントは、ブロックの中で例外が起きても__exit__は必ず実行されることです。これにより、ファイルやネットワーク接続、ロックなどの解放を確実に行えます。この記事では、実機のPythonで確認しながら、withとコンテキストマネージャを整理します。
withは後始末(クローズや解放)を自動化します。with open(...) as f:が代表例です。- 自作するには、
__enter__(開始)と__exit__(終了)を持つクラスを作ります。 as 変数には、__enter__がreturnした値が入ります。- ブロック内で例外が起きても
__exit__は必ず実行されます(確実な後始末)。 - 簡単に作るなら
@contextlib.contextmanagerとyieldを使います。 - 複数まとめて
with A() as a, B() as b:と書けます。
代表例のファイル操作はファイルの読み書き(with open)、__enter__などの特殊メソッドはクラス(class)、後始末が関わる例外処理(try/except)もあわせて参考になります。
with文とは(後始末の自動化)
もっともよく使うwithがファイルです。with open(...)を使うと、ブロックを抜けたときに自動でclose()が呼ばれます。これはtry-finallyで閉じ忘れを防ぐのと同じ効果を、短く安全に書けます。
# with を使うと、抜けるときに自動で閉じられる
with open("data.txt", encoding="utf-8") as f:
content = f.read()
# ここで f は自動的に閉じられている(close 不要)
# with を使わない場合は、自分で閉じる必要がある
f = open("data.txt", encoding="utf-8")
content = f.read()
f.close() # 忘れたり、途中でエラーが起きると閉じられない
with open(...) as f:のブロックを抜けると、fは自動的に閉じられます。close()を書く必要も、途中でエラーが起きたときの閉じ忘れを心配する必要もありません。この「自動で後始末する」仕組みを、自分のクラスにも持たせられるのがコンテキストマネージャです。
自作する:__enter__と__exit__
コンテキストマネージャは、__enter__(withに入るとき)と__exit__(抜けるとき)の2つのメソッドを持つクラスで作ります。__enter__が返した値がas 変数に入ります。
class Timer:
def __enter__(self):
print("開始(__enter__)")
return self # この戻り値が as の変数に入る
def __exit__(self, exc_type, exc_val, exc_tb):
print("終了(__exit__)")
return False # 例外を抑制しない(後述)
with Timer() as t:
print("本体の処理")
# 開始(__enter__)
# 本体の処理
# 終了(__exit__)
実機でも、with Timer() as t:で__enter__→本体→__exit__の順に実行されました。__enter__はwithに入るときに呼ばれ、その戻り値がas tのtに入ります。__exit__はブロックを抜けるときに呼ばれ、ここで後始末(ファイルを閉じる、接続を切る、ロックを解放するなど)を行います。__exit__はexc_type・exc_val・exc_tbという3つの引数を受け取り、これでブロック内のエラーの情報が分かります。
例外が起きても__exit__は実行される
コンテキストマネージャのもっとも重要な性質がこれです。ブロックの中で例外が起きても、__exit__は必ず実行されます。だからこそ、エラー時でも確実に後始末できます。
class Timer:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("__exit__ 実行(exc_type =", exc_type, ")")
return False
try:
with Timer() as t:
print("本体(この後で例外)")
raise ValueError("エラー発生")
except ValueError as e:
print("except で捕捉:", e)
# 本体(この後で例外)
# __exit__ 実行(exc_type = <class 'ValueError'> )
# except で捕捉: エラー発生
実機で確認したところ、ブロック内でraise ValueError(...)を起こしても、__exit__はきちんと実行され、そのあとに例外がexceptへ伝わりました。__exit__の引数exc_typeには、発生した例外の種類(<class 'ValueError'>)が入っていました。正常終了したときは、これらの引数はすべてNoneになります。つまり__exit__は、正常・異常のどちらでも必ず呼ばれ、引数を見ればどちらだったか判断できるのです。この保証があるからこそ、with openはエラーが起きてもファイルを確実に閉じられます。ファイル・データベース接続・ロックなど「必ず後始末が必要なリソース」は、コンテキストマネージャで管理するのが安全です。
contextlib.contextmanagerで簡単に作る
クラスを書くのが手間なときは、contextlibの@contextmanagerデコレータを使うと、1つの関数で簡単にコンテキストマネージャを作れます。yieldの前が__enter__、後が__exit__に相当します。
from contextlib import contextmanager
@contextmanager
def managed():
print("前処理(yield の前 = enter)")
yield "リソース" # この値が as に入る
print("後処理(yield の後 = exit)")
with managed() as r:
print("本体: r =", r)
# 前処理(yield の前 = enter)
# 本体: r = リソース
# 後処理(yield の後 = exit)
実機でも、@contextmanagerを付けた関数で、yieldの前(前処理)→本体→yieldの後(後処理)の順に実行されました。yieldで渡した値がas rに入り、yieldの前が開始処理、後が後始末になります。クラスで__enter__・__exit__を書くより短く済むため、簡単なコンテキストマネージャはこちらが手軽です。なお、後始末を確実にするため、yieldをtry-finallyで囲んでfinallyに後処理を書くと、本体で例外が起きても後処理が実行されてより安全です。
複数のコンテキストマネージャ
複数のリソースを同時に使うときは、カンマで区切って1つのwithに並べられます。入った逆の順番で__exit__が呼ばれます。
# 2つのファイルを同時に開く(読み込み元と書き込み先など)
with open("in.txt", encoding="utf-8") as src, \
open("out.txt", "w", encoding="utf-8") as dst:
dst.write(src.read())
# 両方とも自動的に閉じられる
# 自作のものも同様に並べられる
# with Timer() as a, Timer() as b:
# ...
実機でも、with A() as a, B() as b:のように並べると、a→bの順で__enter__が呼ばれ、終了時はb→aの逆順で__exit__が呼ばれました。ファイルのコピーのように「入力と出力を同時に開く」場面で便利です。1行が長くなるときは、\で改行するか、全体を丸かっこで囲んで複数行に分けられます。
主なポイントまとめ
withとコンテキストマネージャの要点をまとめます。
| 項目 | ポイント |
|---|---|
| 役割 | 後始末(クローズ・解放)を自動化 |
| 開始 / 終了 | __enter__ / __exit__ |
asの値 |
__enter__の戻り値 |
| 例外時 | __exit__は必ず実行される |
| 簡単に作る | @contextmanager+yield |
| 複数 | with A() as a, B() as b: |
よくある失敗
__exit__を定義し忘れる
コンテキストマネージャには__enter__と__exit__の両方が必要です。片方だけだとエラーになります。
__exit__の引数を省く
__exit__(self, exc_type, exc_val, exc_tb)と3つの引数が必要です。
as の値を勘違いする
asに入るのは__enter__の戻り値です。return selfなどで明示します。
後始末をwithの外に書く
後始末は__exit__に書きます。これで例外時も確実に実行されます。
contextmanagerでyieldを2回書く
@contextmanagerの関数ではyieldは1回だけです。前が開始、後が後始末です。
よくある質問
with open(...) as f:と書くと、ブロックを抜けるときに自動でファイルが閉じられ、エラーが起きても閉じ忘れません。__enter__(開始時の処理)と__exit__(終了時の後始末)の2つのメソッドを持つクラスを作ります。__enter__の戻り値がasの変数に入ります。より簡単には、@contextlib.contextmanagerデコレータとyieldを使って関数1つで作れます。__exit__は、ブロック内で例外が起きても必ず実行されます。__exit__の引数exc_typeなどに例外の情報が入るため、エラーかどうかを判断して後始末できます。この保証があるため、withはエラー時でもファイルを確実に閉じられます。__exit__がTrueを返すと例外がwithの外に伝わらず、後の処理が実行されました。通常はFalse(または何も返さない)にして、例外はそのまま伝えるのが基本です。with open("a") as f1, open("b") as f2:のようにカンマで区切って並べます。どちらも自動的に閉じられ、終了時は開いた逆の順番で後始末されます。1行が長いときは、全体を丸かっこで囲んで複数行に分けられます。まとめ
withは後始末を自動化。with open(...) as f:が代表例です。- 自作は
__enter__(開始)と__exit__(終了)を持つクラスで。 asには__enter__の戻り値が入ります。- 例外が起きても
__exit__は必ず実行され、確実に後始末できます。 - 簡単に作るなら
@contextmanager+yield。複数はwith A() as a, B() as b:。
コンテキストマネージャは、「使ったら必ず後始末する」処理を安全に書くためのPythonの仕組みです。ファイルや接続、ロックなど、確実に解放したいリソースはwithで管理するのが鉄則です。__enter__・__exit__の役割と「例外時も後始末される」性質を押さえれば、自分のクラスにもこの安全な仕組みを取り入れられます。

