ファイルを開くとき、with open(...) as f: と書くのはもうおなじみかもしれません。ブロックを抜けると自動でファイルを閉じてくれる、あの便利な書き方です。
では、このwith文を自分で作れるとしたらどうでしょうか。処理の前に準備をして、終わったら必ず後片付けをする仕組みを、自分のコードにも持ち込めたら便利だと思いませんか。
Pythonには、そのための標準ライブラリとしてcontextlibが用意されています。今回は、with文の仕組みをやさしく整理したうえで、contextlibでよく使う機能を順番に見ていきましょう。
そもそもwith文は何をしているのか¶
contextlibの話に入る前に、with文の中身を少しだけのぞいておきます。
with文は、ブロックに入るときと出るときに、決まった処理を自動で呼び出す構文です。ブロックの中でエラーが起きても、出るときの処理は必ず実行されます。
この入口と出口の処理を持っているオブジェクトを、コンテキストマネージャと呼びます。open関数が返すファイルオブジェクトも、コンテキストマネージャの一種です。
try-finallyで書くと何行になるか¶
with文がない世界を想像すると、ありがたみがよくわかります。ファイルを確実に閉じるには、try文とfinallyを組み合わせて書く必要があります。
実際に、同じ処理を2通りで書き比べてみましょう。
# with文を使わない書き方
f = open("memo.txt", "w", encoding="utf-8")
try:
f.write("こんにちは")
finally:
f.close() # エラーが起きても必ず閉じる
# with文を使った書き方
with open("memo.txt", "w", encoding="utf-8") as f:
f.write("こんにちは")
どちらも同じ動きですが、with文のほうが短く、閉じ忘れる余地がありません。後片付けを書き忘れられない形にするのが、with文のいちばんの価値です。
open関数とwith文の基本をおさらいしたい方は、こちらの記事が参考になります。【関連記事】Pythonのファイル読み書きとは?open()とwith文の基本を初心者向けに解説
__enter__と__exit__という2つのメソッド¶
その正体は、__enter__と__exit__という2つの特殊メソッドを持ったオブジェクトです。with文に入るときに__enter__が、出るときに__exit__が呼ばれます。
__exit__には、ブロックの中で起きた例外の情報が渡されます。例外がなければ、3つの引数はすべてNoneです。
大事なのは、__exit__が真の値を返すと例外が握りつぶされるというルールです。
クラスでこれを書くと、少し行数が増えます。処理時間を測るタイマーを例に見てみましょう。
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
elapsed = time.perf_counter() - self.start
print(f"経過時間: {elapsed:.3f}秒")
return False # 例外は握りつぶさずに外へ伝える
with Timer():
sum(range(1_000_000))
毎回クラスを作るのは少し面倒ですよね。そこで登場するのがcontextlibです。
@contextmanagerなら関数1つで作れる¶
contextlibでいちばんよく使うのが、@contextmanagerというデコレータです。これを使うと、クラスを書かずに関数だけでコンテキストマネージャを作れます。
先ほどのタイマーを、@contextmanagerで書き直してみます。
import time
from contextlib import contextmanager
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield # ここでwithブロックの中身が実行される
finally:
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.3f}秒")
with timer("集計処理"):
sum(range(1_000_000))
ポイントは、関数の途中にあるyieldです。yieldより前が入口の処理、yieldより後ろが出口の処理になります。
yieldの位置で関数はいったん止まり、そのあいだにwithブロックの中身が実行されます。ブロックが終わると、関数の続きから再開する流れです。
yieldの動きそのものに慣れていない方は、ジェネレータの記事を先に読むとイメージしやすくなります。【関連記事】ジェネレータ(yield)っていつ使うの?巨大なデータをメモリ節約して扱う方法
yieldで値を渡すとasで受け取れる¶
yieldに値を渡すと、それがwith文のasで受け取る値になります。準備したものを、ブロックの中へ手渡すイメージです。
たとえば、データベースの接続を開いて渡し、最後に必ず閉じる関数は次のように書けます。
import sqlite3
from contextlib import contextmanager
@contextmanager
def open_db(path):
conn = sqlite3.connect(path)
try:
yield conn
finally:
conn.close() # 途中でエラーが起きても必ず閉じる
with open_db(":memory:") as conn:
conn.execute("CREATE TABLE users (name TEXT)")
conn.execute("INSERT INTO users VALUES ('sato')")
print(conn.execute("SELECT name FROM users").fetchall())
yieldは1回だけ、というのが公式ドキュメントのルールです。
try-finallyを忘れると後片付けが飛ばされる¶
ここは、初心者がとてもつまずきやすいポイントです。yieldをtry-finallyで囲まないと、ブロックの中で例外が起きたときに後片付けが実行されません。
なぜなら、ブロックで起きた例外は、関数の中のyieldの位置で改めて投げ直されるからです。finallyがなければ、yieldより後ろの行にはたどり着けません。
私も昔、ロックを解放する処理をyieldの後ろにそのまま書いてしまったことがあります。テストでは問題なく動いていたのに、本番で一度だけ例外が起きた日から、ロックが外れずに後続のバッチがすべて待ちぼうけになりました。
@contextmanagerを書くときは、yieldをtry-finallyで囲むのを最初の型にすると覚えておくと安心です。
クラスと@contextmanagerの使い分け¶
ここまでで、コンテキストマネージャの作り方を2つ見てきました。どちらを選べばいいのか、特徴を表で比べてみます。
| 観点 | クラスで書く | @contextmanagerで書く |
|---|---|---|
| コードの長さ | 長くなりがち | 短く書ける |
| 状態の持ち方 | 属性として自由に持てる | 関数のローカル変数に持つ |
| 例外の扱い | __exit__の戻り値で決める | try-exceptで捕まえる |
| 何度も使い回す | 同じインスタンスを再利用しやすい | 呼ぶたびに作り直す |
| 向いている場面 | 部品として配布するもの | ちょっとした準備と後片付け |
日々のコードで使うなら、まずは@contextmanagerで十分です。状態をたくさん持つ場合や、ライブラリとして他人に使ってもらう場合に、クラスを検討するとよいでしょう。
なお、@contextmanagerで作ったものは、デコレータとしても使えます。関数の頭に@timer("処理名")と付ければ、その関数全体の時間を測れます。
デコレータの仕組みそのものは、こちらで詳しく解説しています。【関連記事】Pythonのデコレータ(@)って何?関数の前後で処理を追加する不思議な記法の正体
すぐに使える便利な道具たち¶
contextlibには、自分で作らなくてもそのまま使える道具も入っています。よく使うものを、追加されたバージョンと一緒に表にまとめました。
| 名前 | できること | 追加されたバージョン |
|---|---|---|
| suppress | 指定した例外を無視する | 3.4 |
| redirect_stdout | printの出力先を一時的に切り替える | 3.4 |
| closing | closeメソッドを持つものを確実に閉じる | 古くからある |
| ExitStack | 数が決まっていない後片付けをまとめる | 3.3 |
| nullcontext | 何もしないコンテキストマネージャ | 3.7 |
| chdir | 作業フォルダを一時的に移動する | 3.11 |
この中から、特に出番の多いものを順に紹介します。
suppressで想定内の例外だけを無視する¶
ファイルを消したいけれど、もともと存在しなければそれで構わない、という場面はよくあります。try-exceptで書くと4行になる処理が、suppressなら2行で済みます。
具体的には、次のように書きます。
import os
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("cache.tmp") # なければ何もせずに次へ進む
ただし、suppress(Exception)のように広く取りすぎると、本当に気づくべきエラーまで消えてしまいます。
例外をまとめて握りつぶす書き方がなぜまずいのかは、こちらの記事で詳しく触れています。【関連記事】Pythonの例外処理のアンチパターン5選をご紹介!|初心者がやりがちな「べからず集」
redirect_stdoutでprintの出力を横取りする¶
redirect_stdoutを使うと、ブロックの中のprintの出力先を一時的に変えられます。自分では書き換えられない関数が、画面にしか結果を出さないときに役立ちます。
たとえば、help関数の出力を文字列として受け取るには、次のように書きます。
import io
from contextlib import redirect_stdout
buffer = io.StringIO()
with redirect_stdout(buffer):
help(len)
text = buffer.getvalue()
print(text.splitlines()[0])
ただし、これはプログラム全体の標準出力を切り替える仕組みです。公式ドキュメントでも、ライブラリのコードや複数のスレッドで動く処理には向かないと注意されています。
chdirで作業フォルダを一時的に移動する¶
Python 3.11からは、作業フォルダを一時的に移動するchdirが加わりました。ブロックを抜けると、元のフォルダへ自動で戻ってくれます。
これもプログラム全体の状態を変えるので、スレッドや非同期処理との組み合わせは避けましょう。
ExitStackで数の決まらない後片付けをまとめる¶
複数のファイルを同時に開きたいとき、with文を入れ子にすると右へ右へと深くなっていきます。しかも、開くファイルの数が実行時まで決まらない場合は、入れ子で書くこと自体ができません。
そんなときに頼りになるのがExitStackです。enter_contextで登録したコンテキストマネージャを、ブロックを抜けるときにまとめて片付けてくれます。
複数のCSVファイルの先頭行を読む例で見てみましょう。
from contextlib import ExitStack
filenames = ["a.csv", "b.csv", "c.csv"]
with ExitStack() as stack:
files = [
stack.enter_context(open(name, encoding="utf-8"))
for name in filenames
]
for f in files:
print(f.name, f.readline().strip())
# ここで、開いたファイルがすべて閉じられる
片付けは、登録したのとは逆の順番で行われます。入れ子のwith文を書いたときと同じ順番なので、依存関係のある後片付けでも安心です。
途中の3つ目のファイルが開けずにエラーになった場合も、それまでに開いた2つはきちんと閉じられます。途中で失敗しても開いた分だけ確実に閉じるのが、ExitStackの強みです。
callbackで普通の関数も登録できる¶
ExitStackには、コンテキストマネージャではない普通の関数も登録できます。callbackメソッドに関数を渡すと、ブロックを抜けるときに呼び出してくれます。
一時的に作ったファイルを最後に消したい、といった用途にぴったりです。一時ファイルそのものを安全に作る方法は、tempfileの記事で紹介しています。【関連記事】Pythonのtempfileとは?一時ファイルと一時フォルダを安全に作って自動で片付ける方法を初心者向けに解説
実務で使うときの注意点¶
最後に、contextlibを実務で使うときに気をつけたい点を整理しておきます。どれも一度ハマると原因を見つけにくいものばかりです。
例外を捕まえたら投げ直すのを忘れない¶
@contextmanagerの中でexceptを使って例外を捕まえた場合、ログを出すだけなら必ずraiseで投げ直します。投げ直さないと、with文は例外が処理済みだと判断して、何事もなかったかのように次の行へ進みます。
これは公式ドキュメントにもはっきり書かれている仕様です。エラーが起きたのに処理が成功したように見える、という一番やっかいな不具合につながるので気をつけましょう。
後片付けはwith文に寄せる習慣を¶
10年ほど開発に関わってきて感じるのは、後片付けの漏れは、動作確認ではまず見つからないということです。エラーが起きたときにだけ表に出るので、気づくのはたいてい本番環境になってからです。
だからこそ、開いたら閉じる、始めたら終わらせる、という処理を見かけたら、with文にできないか考える癖をつけておきましょう。contextlibは、その考え方を自分のコードに持ち込むための道具です。
まとめ¶
今回は、Pythonのcontextlibを使って、with文で使えるコンテキストマネージャを作る方法を紹介しました。@contextmanagerを使えば、yieldをはさんだ関数1つで準備と後片付けをまとめられます。
あわせて、例外を無視するsuppress、出力を横取りするredirect_stdout、後片付けをまとめるExitStackといった道具も見てきました。どれも標準ライブラリなので、今日からすぐに使えます。
まずは、手元のコードでtry-finallyを書いている場所を探してみてください。そこがcontextlibの出番かもしれません。
ここまでお読みいただきありがとうございました。