自動で後始末してくれる構文、with
プログラムで扱うリソース(ファイル、ネットワーク接続、データベースのハンドル、ロックなど)は、使い終わったら必ず閉じる必要があります。閉じ忘れると、メモリリークが起きたり、他のプロセスをブロックするロックを握り続けたり、クラッシュ時にファイルが壊れたりと、いいことがありません。こうした後始末を自動でやってくれるのが、Python の with 文です。
with 文の使い方として、誰もが最初に出会うのがファイルの読み込みでしょう。
with open("notes.txt") as f:
contents = f.read()
print(contents)
このとき、自動的に2つのことが起こります。ブロックに入るときに open() がファイルオブジェクトを返し、それが f に束縛されます。そしてブロックを抜けるとき——正常に終了しようが、途中で return しようが、例外が発生しようが——Pythonが自動で f.close() を呼んでくれます。
これだけです。with 文の本質はまさにここにあります。
with文が置き換えるもの
コンテキストマネージャーが登場する前は、同じことを安全にやろうとすると try/finally を書く必要がありました。
f = open("notes.txt")
try:
contents = f.read()
print(contents)
finally:
f.close()
「ファイルを開いて、終わったら閉じる」というだけで5行のお作法を書かされる。規模が大きくなって open の数が増えてくると、このしんどさが見えてきます。with 文を使えばコードは短くなり、ミスも起きにくく、何よりクローズ処理を忘れようがありません。
python with文で複数ファイルを同時に開く
1つの with 文の中で、複数のコンテキストマネージャーをまとめて使うこともできます。
with open("input.txt") as src, open("output.txt", "w") as dst:
dst.write(src.read().upper())
どちらのファイルも with に入った時点でオープンされ、抜けるときに両方ともクローズされます。1つ目の open が成功して2つ目が例外を投げた場合でも、Python はちゃんと1つ目を閉じてくれます。途中までセットアップが済んだ状態でも、仕組みがきちんと後始末してくれるわけです。
扱うリソースが増えてくる場合は、丸カッコを使った書き方(Python 3.10 以降)の方が見やすくなります。
with (
open("a.txt") as a,
open("b.txt") as b,
open("c.txt") as c,
):
...
コンテキストマネージャーとは何なのか
__enter__ と __exit__ を定義しているオブジェクトは、すべてコンテキストマネージャーです。プロトコル自体は拍子抜けするほどシンプルです。
__enter__(self)はwithブロックに入るときに呼ばれます。この戻り値がas 名前で束縛される値になります。__exit__(self, exc_type, exc_value, traceback)はブロックを抜けるときに、どんな抜け方をしたかに関係なく必ず呼ばれます。例外で抜けた場合は例外情報が引数として渡されるので、コンテキストマネージャー側で中身を確認したり、必要に応じて握りつぶしたりできます。
以下は、ブロックの実行時間を計測するだけの最小限のコンテキストマネージャーです。
with Timer(): を書くと、オブジェクトが作られ、__enter__ が呼ばれ、ブロック内の処理が走って、最後に __exit__ が呼ばれます。ファイルもロックも出てきません。要するに「何かをやって、かかった時間を測る」だけの薄いラッパーです。
contextlib.contextmanager でもっと手軽に
コンテキストマネージャーを作るたびにクラスを書くのは、正直ちょっと大げさです。そこで登場するのが contextlib.contextmanager。ジェネレーター関数をそのままコンテキストマネージャーに変えてくれる便利なデコレーターで、1 つの yield が「前処理」と「後処理」の境目になります。
yield より前が __enter__ に相当する処理、そのあとが __exit__ に相当する処理です。try / finally で囲むことで、with ブロックの中で例外が発生しても後片付けが必ず走るようになります。
自分でコンテキストマネージャーを書く場面では、だいたいこの形で事足ります。まずはデコレーター版を試してみて、ジェネレーターでは表現できないときだけクラスベースに切り替える、という順番がおすすめです。
一時的に値を差し替える
「何かをセットして、使って、元に戻す」——これはよくあるパターンですね。コンテキストマネージャーを使うと、この流れをすっきり書けます。
「設定して、あとで元に戻す」というパターンは、環境変数・ログレベル・フィーチャーフラグ・テスト用のフィクスチャなど、コンテキストマネージャーと非常に相性がいいです。呼び出し側は後始末を意識しなくて済みます。
例外を握りつぶす
__exit__ メソッドが True を返すと、Python に「この例外はこちらで処理したから、なかったことにしていいよ」と伝えられます。多用するとコードの臭いになりがちですが、contextlib.suppress はまさにこの仕組みで動いています。
suppress(FileNotFoundError) を使うと、FileNotFoundError が発生しても何も起きなかったことにできます。「ダメ元で試してみて、失敗しても気にしない」――本当にそういう扱いでいい処理にだけ使ってください。中身を理解していない例外を握りつぶすために使うのはやめましょう。
よく出会うその他のコンテキストマネージャー
python のコンテキストマネージャーは、意識し始めると標準ライブラリのあちこちで顔を出します。
import threading
from pathlib import Path
# Locks — guarantee release even if the critical section raises.
lock = threading.Lock()
with lock:
...
# tempfile — delete the temp file when you're done.
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
path = Path(tmp) / "scratch.txt"
path.write_text("hello")
# Database connections — close the connection (or end the transaction).
import sqlite3
with sqlite3.connect(":memory:") as conn:
conn.execute("CREATE TABLE t (x INTEGER)")
サードパーティ製のライブラリも同じ流儀に従っています。with something as x: という書き方を見かけたら、たいていは「このブロックの間だけ x を使って、抜けたら後片付けする」という意味だと思って間違いありません。
with文を使わないほうがいいケース
- そもそもセットアップと後片付けがない場合。 何の意味もないコードをコンテキストマネージャーでくるんでも、ノイズが増えるだけです。
- 複数の無関係な箇所でリソースを使い回したい場合。 長いスクリプト全体を
withで囲んでしまうと、後片付けのスコープがどこまでなのか分かりづらくなります。こういうときは、リソースを保持するクラスを用意したほうが素直です。 - デコレーターのほうがしっくりくる場合。 リトライ・ロギング・計測のように繰り返し出てくるパターンは、関数の中で
with ...:するより、関数に@decoratorを付けたほうが自然に読めることがあります。呼び出し側で読みやすいほうを選びましょう。
とはいえ、ほとんどの場面では with が正解です。例外的なケースは、意識して見ていればすぐに気づけるようになります。
次は実際のファイル操作へ
これで with open(...) as f: の裏側で何が起きているかが分かりました。実際に with文を使うシーンの9割はこの形です。次の章では、この書き方を使ってディスク上のファイルを読み書きしたり、中を操作したりする方法を見ていきます。
よくある質問
Pythonのwith openは何をしているの?
with openは何をしているの?with open(path) as f: と書くと、ファイルを開いてブロックの間だけ f に束縛してくれます。ブロックを抜けるとき——正常終了でも例外が出たときでも——Pythonが自動でファイルを閉じてくれるので、f.close() を書く必要はありません。with を使えばクローズが保証されます。
なぜ普通のopen()ではなくwithを使うべき?
open()ではなくwithを使うべき?途中で例外が飛んでも with なら確実にファイルを閉じてくれるからです。素の open() だと、エラー時も含めてすべての経路で自分が close() を呼ぶ責任を負うことになります。with の方が安全で、コードも短く済みます。
1つのwith文で複数のファイルを開くには?
with文で複数のファイルを開くには?コンテキストマネージャーをカンマで並べるだけでOKです。with open('a.txt') as a, open('b.txt') as b: のように書けば、入るときに両方開かれ、抜けるときに逆順で閉じられます。with をネストする必要がなくなり、複数リソースを同時に扱いたいときに便利です。