- Python
Python with文の使い方|コンテキストマネージャと__enter__
Pythonのwith文とは、「使い始めの準備」と「使い終わりの後片付け」を自動で実行してくれる構文です。 with open("memo.txt") as f: と書くと、ブロックを抜けた時点でファイルが必ず閉じられます。途中で例外が起きても、return で抜けても、後片付けは実行されます。
この「準備と後片付け」を受け持つオブジェクトをコンテキストマネージャと呼び、その正体は __enter__ と __exit__ という2つのメソッドを持つオブジェクトです。この記事では、with ありとなしのコードを並べ、3つの処理がどの順で呼ばれるかを print の実行結果で確かめます。 例外が起きたときに __exit__ の3つの引数へ何が入るか、True を返すと例外が外へ伝わらなくなる挙動も再現しました。自作クラス版と @contextmanager 版のコピペ用コード、括弧付きの複数 with、async with、with で使える標準機能の一覧表まで、すべて Python 3.14.7 で実行した結果つきでまとめています。
with文の早見表
| 知りたいこと | 答え |
|---|---|
| 基本の形 | with 式 as 変数: の下に、字下げして本体を書く |
| 呼ばれる順番 | __enter__ → 本体 → __exit__(例外や return で抜けても __exit__ は呼ばれる) |
as の右に入るもの |
__enter__ の戻り値(式そのものではない)。不要なら as は省略できる |
__exit__ の引数 |
(exc_type, exc_value, traceback)。例外がなければ3つとも None |
__exit__ の戻り値 |
True(真の値)を返すと例外を抑制する。False や None なら例外はそのまま外へ伝わる |
| 複数まとめて書く | with A() as a, B() as b:。入るのは左から、出るのは右から |
| 複数行に分ける | with (A() as a, B() as b): と括弧で囲む(3.10以降) |
| 非同期の処理 | async with。__aenter__ と __aexit__ が呼ばれる |
| 自作する方法 | クラスに2つのメソッドを書く、または @contextlib.contextmanager を関数に付ける |
| with を抜けたあとの変数 | 変数は残る。ファイルなら閉じた状態で残る |
with でよく使う標準機能は次のとおりです(詳しい一覧は後半にあります)。
| 書き方 | 抜けるときに行われること |
|---|---|
with open(path) as f: |
ファイルを閉じる |
with lock: |
ロックを解放する |
with sqlite3.connect(...) as con: |
コミット、例外時はロールバック(接続は閉じない) |
with tempfile.TemporaryDirectory() as d: |
一時フォルダを中身ごと削除する |
with contextlib.suppress(FileNotFoundError): |
指定した例外を抑制する |
with contextlib.redirect_stdout(buf): |
標準出力の向き先を元に戻す |
with contextlib.ExitStack() as stack: |
登録した後片付けを逆順に実行する |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python with文の使い方|コンテキストマネージャと__enter__」
with文とは:ありとなしを並べて比べる
ファイルに書き込む処理を、with なしと with ありで並べます。
# with なし:閉じる処理を自分で書く
f = open("memo.txt", "w", encoding="utf-8")
try:
f.write("hello\n")
finally:
f.close() # 例外が起きても必ず閉じる
# with あり:閉じる処理は書かなくてよい
with open("memo.txt", "w", encoding="utf-8") as f:
f.write("hello\n")
with なしで同じ安全さを得るには、try と finally を書いて close() を呼ぶ必要があります。close() を書き忘れたり、try の外に書いたりすると、途中で例外が起きたときにファイルが開いたままになります。with 文は、この「try / finally と後片付けの呼び出し」を1行にまとめたものと考えると、仕組みがつかみやすくなります。
| 比べる点 | with なし(try/finally) | with あり |
|---|---|---|
| 行数 | 5行 | 2行 |
| 後片付けの書き忘れ | 起こりうる | 起こらない(自動で呼ばれる) |
| 例外が起きたとき | finally を書いていれば後片付けされる |
必ず後片付けされる |
| 後片付けの中身を知る必要 | ある(close()、release() など) |
ない(オブジェクト側が知っている) |
| 字下げの範囲 | 使っている範囲が読み取りにくい | 字下げされた部分が「使っている範囲」 |
呼ばれる順番を実行結果で確かめる
__enter__ と __exit__ に print を入れたクラスを用意して、with ありと with なしを続けて実行します。
class Room:
def __init__(self, name):
self.name = name
print(f"0. __init__ {name}")
def __enter__(self):
print("1. __enter__ 入る準備")
return self # as の右に入る値
def __exit__(self, exc_type, exc_value, traceback):
print("3. __exit__ 後片付け", exc_type, exc_value, traceback)
return False # 例外はそのまま外へ伝える
print("--- with あり")
with Room("会議室A") as room:
print("2. 本体 ", room.name)
print("4. with の外")
print("--- with なし(try/finally で同じことを書く)")
room = Room("会議室B")
room.__enter__()
try:
print("2. 本体 ", room.name)
finally:
room.__exit__(None, None, None)
print("4. try の外")
--- with あり
0. __init__ 会議室A
1. __enter__ 入る準備
2. 本体 会議室A
3. __exit__ 後片付け None None None
4. with の外
--- with なし(try/finally で同じことを書く)
0. __init__ 会議室B
1. __enter__ 入る準備
2. 本体 会議室B
3. __exit__ 後片付け None None None
4. try の外
どちらも __init__ → __enter__ → 本体 → __exit__ → 外 の順で、まったく同じ出力になりました。with 文がしていることを順に並べると、次のようになります。
| 順番 | with 文がすること | 上のコードでの対応 |
|---|---|---|
| 1 | with の右の式を評価して、コンテキストマネージャを得る |
Room("会議室A") で __init__ が動く |
| 2 | __enter__() を呼ぶ |
「1. __enter__」が表示される |
| 3 | as があれば、__enter__() の戻り値を変数に入れる |
room に self が入る |
| 4 | 字下げされた本体を実行する | 「2. 本体」が表示される |
| 5 | __exit__() を呼ぶ。例外がなければ引数は3つとも None |
「3. __exit__ … None None None」 |
今回開いた公式の言語リファレンス(複合文の with 文の節)でも、with 文は次のコードと「意味として等価」と説明されています。
manager = (EXPRESSION)
enter = manager.__enter__
exit = manager.__exit__
value = enter()
hit_except = False
try:
TARGET = value
SUITE
except:
hit_except = True
if not exit(*sys.exc_info()):
raise
finally:
if not hit_except:
exit(None, None, None)
EXPRESSION が with の右の式、TARGET が as の変数、SUITE が本体です。例外が起きたら exit に例外の情報を渡し、戻り値が偽なら raise で例外を投げ直す、という流れが読み取れます。この部分が、次に見る「3つの引数」と「True を返すと抑制される」挙動の出どころです。
asに入るのは __enter__ の戻り値
as の右の変数に入るのは、with の右に書いた式そのものではなく、__enter__() が返した値です。open() が返すファイルオブジェクトは __enter__ で自分自身を返すので、どちらでも同じに見えます。違いがわかる例を実行します。
class ReturnsSelf:
def __enter__(self):
return self
def __exit__(self, *exc):
return False
class ReturnsNothing:
def __enter__(self):
pass # return を書いていない
def __exit__(self, *exc):
return False
class ReturnsTuple:
def __enter__(self):
return ("a.txt", 3) # 自分以外の値を返してもよい
def __exit__(self, *exc):
return False
with ReturnsSelf() as x:
print(type(x).__name__)
with ReturnsNothing() as x:
print(x)
with ReturnsTuple() as (name, count):
print(name, count)
with open(__file__, encoding="utf-8") as f:
first = f.readline()
print(f.closed, len(first)) # with を抜けても変数 f と first は残る
ReturnsSelf
None
a.txt 3
True 19
自作クラスで as の変数が None になるときは、__enter__ に return self を書いていないことがほとんどです。また、with はスコープ(変数の有効範囲)を作りません。 ブロックを抜けたあとも f は残っていて、閉じた状態になっています。閉じたファイルに書き込もうとすると ValueError: I/O operation on closed file. になります。
例外が起きたときの後片付けメソッドの3引数
本体で例外が起きると、__exit__ には例外の情報が3つ渡されます。戻り値を False にした場合と True にした場合を比べます。
class Show:
def __init__(self, swallow):
self.swallow = swallow
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
print(" exc_type :", exc_type)
print(" exc_value:", repr(exc_value))
print(" traceback:", traceback)
return self.swallow
print("A. 例外なし")
with Show(False):
pass
print("B. 例外あり・False を返す")
try:
with Show(False):
1 / 0
print(" この行は実行されない")
except ZeroDivisionError as e:
print(" 外側で受け取った:", e)
print("C. 例外あり・True を返す")
with Show(True):
1 / 0
print(" この行は実行されない")
print(" 例外は外へ伝わらず、with の次の行へ進む")
A. 例外なし
exc_type : None
exc_value: None
traceback: None
B. 例外あり・False を返す
exc_type : <class 'ZeroDivisionError'>
exc_value: ZeroDivisionError('division by zero')
traceback: <traceback object at 0x7f03bb9a6e40>
外側で受け取った: division by zero
C. 例外あり・True を返す
exc_type : <class 'ZeroDivisionError'>
exc_value: ZeroDivisionError('division by zero')
traceback: <traceback object at 0x7f03bb9a6e80>
例外は外へ伝わらず、with の次の行へ進む
| 引数 | 例外がないとき | 例外があるとき | 使い道 |
|---|---|---|---|
exc_type |
None |
例外のクラス(ZeroDivisionError など) |
例外の種類で処理を分ける |
exc_value |
None |
例外のインスタンス | メッセージを取り出す、ログに残す |
traceback |
None |
トレースバックオブジェクト | 発生した場所を記録する |
Trueを返すと例外が抑制される
C の結果のとおり、__exit__ が True(真と判定される値)を返すと、例外はそこで消えて、with の次の行から処理が続きます。 いわゆる「例外を握りつぶす」状態です。本体の残りの行(1 / 0 の次の print)は実行されません。
__exit__ の戻り値 |
例外の扱い | 向いている場面 |
|---|---|---|
return を書かない(None) |
そのまま外へ伝わる | ほとんどの場合はこれでよい |
False |
そのまま外へ伝わる | 「伝える」ことを明示したいとき |
True |
抑制される | 特定の例外だけを意図して無視するとき |
True を無条件に返すと、想定していない例外まで消えてしまい、原因を調べる手がかりがなくなります。抑制するときは、exc_type を調べて、対象の例外のときだけ True を返す形にします。
class IgnoreMissingKey:
"""KeyError だけを抑制し、ほかの例外は外へ伝える"""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None and issubclass(exc_type, KeyError):
print(" KeyError を抑制:", exc_value)
return True
return False
stock = {"pen": 3}
with IgnoreMissingKey():
print(stock["note"])
print("続行")
try:
with IgnoreMissingKey():
print(stock["pen"] / 0)
except ZeroDivisionError as e:
print("ZeroDivisionError は外へ:", e)
KeyError を抑制: 'note'
続行
ZeroDivisionError は外へ: division by zero
なお、__exit__ の中で例外をもう一度 raise する必要はありません。 偽の値を返せば、with 文が元の例外をそのまま投げ直します。
自作する方法1:クラスで書く
処理時間を測るコンテキストマネージャを、クラスで書いた例です。そのままコピーして使えます。
import time
class Timer:
"""with ブロックの所要時間を測る"""
def __init__(self, label="処理"):
self.label = label
self.elapsed = None
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.elapsed = time.perf_counter() - self.start
status = "完了" if exc_type is None else f"中断({exc_type.__name__})"
print(f"{self.label}: {self.elapsed:.2f} 秒 {status}")
return False
with Timer("集計") as t:
total = sum(range(1_000_000))
time.sleep(0.2)
print(total, t.elapsed > 0.2)
try:
with Timer("読み込み"):
time.sleep(0.1)
raise ValueError("形式が違います")
except ValueError as e:
print("受け取った:", e)
集計: 0.21 秒 完了
499999500000 True
読み込み: 0.10 秒 中断(ValueError)
受け取った: 形式が違います
クラス版は、結果を属性(t.elapsed)に残して with のあとで参照できる点が便利です。例外が起きても __exit__ が呼ばれるので、「中断」の行も表示されています。
自作する方法2:@contextmanagerで書く
contextlib.contextmanager を関数に付けると、yield の前が __enter__、yield の後が __exit__ に相当するコンテキストマネージャになります。yield で渡した値が as の変数に入ります。
from contextlib import contextmanager
@contextmanager
def room(name):
print("1. 入る準備 (yield の前)")
try:
yield name.upper() # as の右に入る値
except ValueError as e:
print(" ValueError を中で処理:", e)
finally:
print("3. 後片付け (yield の後)")
with room("a-101") as r:
print("2. 本体 ", r)
print("---")
with room("b-202") as r:
raise ValueError("定員オーバー")
print("4. 続行")
print("---")
try:
with room("c-303") as r:
raise KeyError("鍵がない")
except KeyError as e:
print("4. 外で受け取った KeyError:", e)
1. 入る準備 (yield の前)
2. 本体 A-101
3. 後片付け (yield の後)
---
1. 入る準備 (yield の前)
ValueError を中で処理: 定員オーバー
3. 後片付け (yield の後)
4. 続行
---
1. 入る準備 (yield の前)
3. 後片付け (yield の後)
4. 外で受け取った KeyError: '鍵がない'
本体で起きた例外は、yield の位置から投げられたかのように関数の中へ届きます。 そのため、except で受ければ抑制(クラス版で True を返すのと同じ)、受けなければ外へ伝わります。2つ目の例では ValueError が中で処理されて「4. 続行」まで進み、3つ目の例では KeyError が外へ伝わりました。
yieldをtry/finallyで囲む
@contextmanager 版でいちばん気をつけたいのが、try / finally の有無です。
from contextlib import contextmanager
@contextmanager
def no_finally():
print("準備")
yield
print("後片付け") # 例外のときは実行されない
@contextmanager
def with_finally():
print("準備")
try:
yield
finally:
print("後片付け") # 例外のときも実行される
for cm in (no_finally, with_finally):
print("---", cm.__name__)
try:
with cm():
raise RuntimeError("途中でエラー")
except RuntimeError as e:
print("受け取った:", e)
--- no_finally
準備
受け取った: 途中でエラー
--- with_finally
準備
後片付け
受け取った: 途中でエラー
try / finally を書かない版では、例外が起きたときに「後片付け」が表示されていません。 例外が yield の行で投げられ、その下の行へ進まないためです。後片付けが必要な処理は、必ず yield を try / finally で囲みます。
クラス版と@contextmanager版の使い分け
| 比べる点 | クラス版 | @contextmanager 版 |
|---|---|---|
| 書く量 | 多い(メソッド2つ) | 少ない(関数1つ) |
| 準備と後片付けの見通し | 2つのメソッドに分かれる | 上から下へ1つの流れで読める |
| 例外の抑制 | __exit__ で True を返す |
yield を try / except で囲む |
| 状態を持つ・結果を残す | 属性に持てる | yield するオブジェクトに持たせる |
| 同じインスタンスを繰り返し使う | 作り方しだいで可能 | 1回の呼び出しにつき1回だけ |
| デコレータとして使う | ContextDecorator を継承すれば可能 |
そのまま可能 |
| 向いている場面 | 状態やメソッドを持たせたい、ライブラリとして公開する | 準備と後片付けだけの小さな処理 |
@contextmanager で作ったものは、関数のデコレータとしても使えます。
import time
from contextlib import contextmanager
@contextmanager
def timer(label):
start = time.perf_counter()
try:
yield
finally:
print(f"{label}: {time.perf_counter() - start:.1f} 秒")
with timer("ブロック"):
time.sleep(0.2)
@timer("関数まるごと")
def job():
time.sleep(0.1)
job()
job()
ブロック: 0.2 秒
関数まるごと: 0.1 秒
関数まるごと: 0.1 秒
複数のwithをまとめて書く
2つ以上のコンテキストマネージャは、カンマで並べて1つの with に書けます。Python 3.10 以降は、全体を括弧で囲んで複数行に分けられます。
class Step:
def __init__(self, name):
self.name = name
def __enter__(self):
print("enter", self.name)
return self.name
def __exit__(self, *exc):
print("exit ", self.name)
print("--- カンマで並べる")
with Step("A") as a, Step("B") as b:
print("body ", a, b)
print("--- 括弧で複数行(Python 3.10 以降)")
with (
Step("A") as a,
Step("B") as b,
Step("C") as c,
):
print("body ", a, b, c)
print("--- 入れ子(1つ目と同じ意味)")
with Step("A") as a:
with Step("B") as b:
print("body ", a, b)
--- カンマで並べる
enter A
enter B
body A B
exit B
exit A
--- 括弧で複数行(Python 3.10 以降)
enter A
enter B
enter C
body A B C
exit C
exit B
exit A
--- 入れ子(1つ目と同じ意味)
enter A
enter B
body A B
exit B
exit A
入るのは左(上)から、出るのは右(下)からです。皿を重ねて、上から順に片付けるのと同じ順番になります。カンマで並べた形は入れ子と同じ意味なので、B の準備で例外が起きても、先に入った A の __exit__ は呼ばれます。
入力ファイルを読みながら出力ファイルへ書く処理は、次のように書けます。
with (
open("input.csv", encoding="utf-8") as src,
open("output.csv", "w", encoding="utf-8") as dst,
):
for line in src:
dst.write(line.upper())
公式の言語リファレンスには、複数の式への対応は 3.1、括弧で囲んで複数行に分ける書き方は 3.10 で加わったと書かれています。3.9 以前でも動かす必要があるコードでは、行末のバックスラッシュで折り返すか、入れ子にします。
async withで非同期の後片付けをする
接続を開く・閉じるといった処理そのものが await を必要とするときは、async with を使います。呼ばれるメソッドは __aenter__ と __aexit__ で、どちらも async def で定義します。
import asyncio
from contextlib import asynccontextmanager
class AsyncRoom:
async def __aenter__(self):
print("1. __aenter__")
await asyncio.sleep(0.1) # 接続などの待ち時間
return self
async def __aexit__(self, exc_type, exc_value, traceback):
await asyncio.sleep(0.1)
print("3. __aexit__ ", exc_type)
return False
@asynccontextmanager
async def async_room():
print("1. 準備")
try:
yield "room"
finally:
await asyncio.sleep(0.1)
print("3. 後片付け")
async def main():
async with AsyncRoom() as r:
print("2. 本体 ", type(r).__name__)
async with async_room() as r:
print("2. 本体 ", r)
asyncio.run(main())
1. __aenter__
2. 本体 AsyncRoom
3. __aexit__ None
1. 準備
2. 本体 room
3. 後片付け
| 比べる点 | with |
async with |
|---|---|---|
| 呼ばれるメソッド | __enter__ / __exit__ |
__aenter__ / __aexit__ |
| メソッドの定義 | def |
async def |
| 書ける場所 | どこでも | async def の中 |
| デコレータで作る | @contextmanager |
@asynccontextmanager |
| 標準ライブラリの例 | open()、threading.Lock() |
asyncio.Lock()、asyncio.TaskGroup()、asyncio.timeout() |
async with に対応したオブジェクトを通常の with に渡すと、Python 3.14.7 では次のメッセージが出ます。取り違えに気づきやすい内容になっています。
TypeError: 'A' object does not support the context manager protocol (missed __exit__ method) but it supports the asynchronous context manager protocol. Did you mean to use 'async with'?
withで使える標準機能の一覧
標準ライブラリには、with に渡せるオブジェクトが数多く用意されています。contextlib の各機能が加わったバージョンは、今回公式ドキュメントのページを開いて確かめました。
| 分類 | 書き方 | 入るとき | 抜けるとき |
|---|---|---|---|
| ファイル | open(path) |
ファイルを開く | 閉じる |
| ファイル | zipfile.ZipFile(path)、tarfile.open(path) |
書庫を開く | 閉じる |
| 一時ファイル | tempfile.TemporaryDirectory() |
一時フォルダを作る | 中身ごと削除する |
| 一時ファイル | tempfile.NamedTemporaryFile() |
一時ファイルを作る | 閉じて削除する |
| 排他制御 | threading.Lock()、threading.RLock() |
ロックを取得する | 解放する |
| 並行処理 | concurrent.futures.ThreadPoolExecutor() |
実行の枠を用意する | すべての処理の完了を待って終了する |
| データベース | sqlite3.connect(path) |
(何もしない) | コミット。例外時はロールバック |
| 数値 | decimal.localcontext() |
計算の設定を複製する | 元の設定に戻す |
| 通信 | socket.socket()、urllib.request.urlopen(url) |
接続を用意する | 閉じる |
| プロセス | subprocess.Popen(args) |
プロセスを起動する | 入出力を閉じて終了を待つ |
| テスト | unittest.mock.patch(target) |
対象を差し替える | 元に戻す |
contextlib |
suppress(例外)(3.4) |
— | 指定した例外を抑制する |
contextlib |
redirect_stdout(f)(3.4)、redirect_stderr(f)(3.5) |
出力先を切り替える | 元に戻す |
contextlib |
closing(obj) |
— | obj.close() を呼ぶ |
contextlib |
nullcontext(値)(3.7) |
値をそのまま返す | 何もしない |
contextlib |
chdir(path)(3.11) |
カレントフォルダを移す | 元に戻す |
contextlib |
ExitStack()(3.3) |
— | 登録した後片付けを逆順に実行する |
contextlib |
AsyncExitStack()(3.7) |
— | 非同期の後片付けも逆順に実行する |
表のうち、open・Lock・一時フォルダ・sqlite3・contextlib の各機能(AsyncExitStack を除く)は下のコードで、ZipFile・ThreadPoolExecutor・decimal.localcontext は別のコードで実行して確かめました。tarfile、NamedTemporaryFile、RLock、socket、urlopen、subprocess.Popen、unittest.mock.patch、AsyncExitStack と、前の節の表にある asyncio の3つは今回は未実行で、一般に知られた仕様として載せています。
open・Lock・一時フォルダ
import os
import tempfile
import threading
with open("memo.txt", "w", encoding="utf-8") as f:
f.write("hello\n")
print("closed:", f.closed)
try:
f.write("追記")
except ValueError as e:
print("ValueError:", e)
lock = threading.Lock()
with lock:
print("with の中 locked:", lock.locked())
print("with の外 locked:", lock.locked())
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "work.txt")
open(path, "w").close()
print("with の中 exists:", os.path.exists(path))
print("with の外 exists:", os.path.exists(d))
os.remove("memo.txt")
closed: True
ValueError: I/O operation on closed file.
with の中 locked: True
with の外 locked: False
with の中 exists: True
with の外 exists: False
DB接続:sqlite3はコミットとロールバック
sqlite3 の接続を with に渡したときに行われるのは、コミットまたはロールバックであり、接続を閉じることではありません。 名前から「閉じてくれる」と思いやすい点なので、実行して確かめます。
import sqlite3
from contextlib import closing
con = sqlite3.connect(":memory:")
con.execute("CREATE TABLE item(name TEXT UNIQUE)")
with con: # 正常に抜けるとコミット
con.execute("INSERT INTO item VALUES ('pen')")
try:
with con: # 例外で抜けるとロールバック
con.execute("INSERT INTO item VALUES ('note')")
con.execute("INSERT INTO item VALUES ('pen')") # 重複
except sqlite3.IntegrityError as e:
print("IntegrityError:", e)
print(con.execute("SELECT name FROM item").fetchall()) # with の後も接続は使える
con.close()
with closing(sqlite3.connect(":memory:")) as con2: # 閉じたいときは closing
print(con2.execute("SELECT 1").fetchone())
try:
con2.execute("SELECT 1")
except sqlite3.ProgrammingError as e:
print("ProgrammingError:", e)
IntegrityError: UNIQUE constraint failed: item.name
[('pen',)]
(1,)
ProgrammingError: Cannot operate on a closed database.
2回目の with では note の追加も取り消され、テーブルには pen だけが残りました。接続を閉じるところまで任せたいときは、contextlib.closing() で包みます。
suppress・redirect_stdout・nullcontext・chdir
import contextlib
import io
import os
import sys
import tempfile
# suppress: 指定した例外だけを抑制する
with contextlib.suppress(FileNotFoundError):
os.remove("no_such_file.txt")
print("この行は実行されない")
print("suppress の後も続行")
# redirect_stdout: print の出力を文字列として受け取る
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
print("画面には出ない")
print("受け取った文字列:", repr(buf.getvalue()))
# nullcontext: with を使う場合と使わない場合を1つの書き方にまとめる
def export(path=None):
cm = open(path, "w", encoding="utf-8") if path else contextlib.nullcontext(sys.stdout)
with cm as out:
out.write("result\n")
export()
# chdir(3.11 以降): カレントフォルダを一時的に移す
here = os.getcwd()
with contextlib.chdir(tempfile.gettempdir()):
print("with の中:", os.getcwd())
print("元に戻った:", os.getcwd() == here)
suppress の後も続行
受け取った文字列: '画面には出ない\n'
result
with の中: /tmp
元に戻った: True
suppress は、try / except / pass の3行を1行で書けるものです。os.remove() で例外が起きた時点でブロックを抜けるので、その下の行は実行されません。抑制したい1行だけを with の中に入れると、意図が伝わりやすくなります。
ExitStack:開く数が実行時に決まるとき
開くファイルの数がリストの長さで決まる場合、with にカンマで並べる書き方は使えません。ExitStack は、あとから登録した後片付けを、抜けるときに逆順でまとめて実行します。
import contextlib
import os
names = ["a.txt", "b.txt", "c.txt"]
for n in names:
with open(n, "w", encoding="utf-8") as f:
f.write(n + "\n")
with contextlib.ExitStack() as stack:
files = [stack.enter_context(open(n, encoding="utf-8")) for n in names]
stack.callback(print, "callback: 最後に登録したものから先に実行される")
print("with の中:", [f.closed for f in files])
print([f.readline().strip() for f in files])
print("with の外:", [f.closed for f in files])
for n in names:
os.remove(n)
with の中: [False, False, False]
['a.txt', 'b.txt', 'c.txt']
callback: 最後に登録したものから先に実行される
with の外: [True, True, True]
enter_context() はコンテキストマネージャの __enter__ を呼んで戻り値を返し、__exit__ を登録します。callback() には、後片付けとして呼びたい関数と引数を渡せます。
つまずきやすい点とエラーメッセージ
with 文で出会いやすい動きを、実行して確かめた結果とあわせてまとめます。エラーメッセージは Python 3.14.7 のものです。
class Trace:
def __enter__(self):
return self
def __exit__(self, *exc):
print(" __exit__ が呼ばれた")
def find():
with Trace():
return "return の値"
print("A. with の中で return")
print(" ", find())
print("B. ループの中で continue / break")
for i in range(2):
with Trace():
if i == 0:
continue
break
print("C. with に渡せないオブジェクト")
try:
with 123:
pass
except TypeError as e:
print(" ", e)
print("D. 後片付けの中で別の例外")
class Broken:
def __enter__(self):
return self
def __exit__(self, *exc):
raise KeyError("後片付け中の例外")
try:
with Broken():
raise ValueError("本体の例外")
except Exception as e:
print(" ", type(e).__name__, e, "/ 元の例外:", repr(e.__context__))
A. with の中で return
__exit__ が呼ばれた
return の値
B. ループの中で continue / break
__exit__ が呼ばれた
__exit__ が呼ばれた
C. with に渡せないオブジェクト
'int' object does not support the context manager protocol (missed __exit__ method)
D. 後片付けの中で別の例外
KeyError '後片付け中の例外' / 元の例外: ValueError('本体の例外')
| 症状・メッセージ | 原因 | 直し方 |
|---|---|---|
as の変数が None |
__enter__ が何も返していない |
return self を書く |
TypeError: 'int' object does not support the context manager protocol (missed __exit__ method) |
with に渡した値が __enter__ / __exit__ を持たない(3.11.15 では括弧の部分がないメッセージ) |
関数の呼び出し忘れ(open と open(...))がないか確かめる |
ValueError: I/O operation on closed file. |
with を抜けたあとでファイルを使っている | 読み書きを with の中に移す |
| 例外が起きたのに何も表示されない | __exit__ が真の値を返している |
return する値を見直す。抑制は例外の種類を絞る |
@contextmanager で後片付けが動かない |
yield を try / finally で囲んでいない |
finally に後片付けを書く |
RuntimeError: generator didn't yield |
@contextmanager の関数が yield に到達していない |
どの経路でも1回 yield するようにする |
RuntimeError: generator didn't stop |
yield が2回以上実行された |
yield は1回にする |
AttributeError: '_GeneratorContextManager' object has no attribute 'args' |
@contextmanager で作ったオブジェクトを変数に入れ、2回 with に渡した |
with のたびに関数を呼び直す |
| 本体の例外が別の例外に置き換わる | __exit__ の中で別の例外が起きた(D の結果) |
後片付けの中の処理を try で囲む。元の例外は __context__ に残る |
sqlite3 の接続が閉じていない |
with はコミットとロールバックだけを行う | closing() で包むか close() を呼ぶ |
このほか、__enter__ の中で例外が起きた場合は、本体も __exit__ も実行されません(実行して確かめました)。準備の途中で確保したものがあるときは、__enter__ の中で try を使って片付けます。
よくある質問
Q. with文を使わずにopenするとどうなりますか?
A. 動作はしますが、close() を自分で呼ぶ必要があります。途中で例外が起きると close() まで進まないため、try / finally で囲む書き方が必要になります。with を使えば、その3〜4行を書かずに同じ安全さが得られます。
Q. コンテキストマネージャとは何ですか?
A. __enter__ と __exit__ の2つのメソッドを持ち、with 文に渡せるオブジェクトのことです。open() が返すファイルオブジェクトや threading.Lock() がその例で、自分でクラスを書くか @contextmanager を使えば自作できます。
Q. asは必ず書く必要がありますか?
A. いいえ。with lock: や with suppress(KeyError): のように、__enter__ の戻り値を使わないときは省略できます。
Q. with文の中で定義した変数は外でも使えますか?
A. 使えます。with は新しいスコープを作らないので、ブロックの中で代入した変数も as の変数も、抜けたあとに残ります。ただしファイルなどは閉じた状態になっています。
まとめ
- with 文は「
try/finallyと後片付けの呼び出し」を1行にまとめた構文で、例外やreturnで抜けても後片付けが実行されます。 - 呼ばれる順番は
__enter__→ 本体 →__exit__です。asの変数には__enter__の戻り値が入ります。 - 例外が起きると
__exit__に(exc_type, exc_value, traceback)が渡り、例外がなければ3つともNoneです。 __exit__が真の値を返すと例外は抑制されます。 抑制するときは例外の種類を絞ります。- 自作はクラス版と
@contextmanager版の2通りです。@contextmanager版はyieldをtry/finallyで囲みます。 - 複数の with は、入るのは左から、出るのは右からです。3.10 以降は括弧で囲んで複数行に書けます。
sqlite3の with は接続を閉じません。 標準機能ごとに「抜けるときに何が行われるか」を一覧表で確かめてから使うと安心です。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python with文の使い方|コンテキストマネージャと__enter__」
株式会社RJCでは、Pythonのwith文とコンテキストマネージャの仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。