【Python】with文とコンテキストマネージャの使い方|__enter__・__exit__・contextlib

【Python】with文とコンテキストマネージャの使い方|__enter__・__exit__・contextlib Python

with文は、使い終わったリソースの後始末を自動でやってくれる仕組みです。もっとも身近な例がファイルの読み書きで、with open(...) as f:と書くと、ブロックを抜けるときに自動でファイルが閉じられます。閉じ忘れの心配がなく、エラーが起きても確実に後始末されます。

このwithで使えるオブジェクトをコンテキストマネージャと呼び、__enter__(開始時)と__exit__(終了時)という2つのメソッドで作れます。ポイントは、ブロックの中で例外が起きても__exit__は必ず実行されることです。これにより、ファイルやネットワーク接続、ロックなどの解放を確実に行えます。この記事では、実機のPythonで確認しながら、withとコンテキストマネージャを整理します。

先に結論

  • with後始末(クローズや解放)を自動化します。with open(...) as f:が代表例です。
  • 自作するには、__enter__(開始)と__exit__(終了)を持つクラスを作ります。
  • as 変数には、__enter__returnした値が入ります。
  • ブロック内で例外が起きても__exit__は必ず実行されます(確実な後始末)。
  • 簡単に作るなら@contextlib.contextmanageryieldを使います。
  • 複数まとめてwith A() as a, B() as b:と書けます。

代表例のファイル操作はファイルの読み書き(with open)__enter__などの特殊メソッドはクラス(class)、後始末が関わる例外処理(try/except)もあわせて参考になります。

スポンサーリンク

with文とは(後始末の自動化)

もっともよく使うwithがファイルです。with open(...)を使うと、ブロックを抜けたときに自動でclose()が呼ばれます。これはtry-finallyで閉じ忘れを防ぐのと同じ効果を、短く安全に書けます。

おなじみの with open
# 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 ttに入ります__exit__はブロックを抜けるときに呼ばれ、ここで後始末(ファイルを閉じる、接続を切る、ロックを解放するなど)を行います。__exit__exc_typeexc_valexc_tbという3つの引数を受け取り、これでブロック内のエラーの情報が分かります。

例外が起きても__exit__は実行される

コンテキストマネージャのもっとも重要な性質がこれです。ブロックの中で例外が起きても、__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__に相当します。

@contextmanager で関数から作る
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__を書くより短く済むため、簡単なコンテキストマネージャはこちらが手軽です。なお、後始末を確実にするため、yieldtry-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:のように並べると、abの順で__enter__が呼ばれ、終了時はba逆順で__exit__が呼ばれました。ファイルのコピーのように「入力と出力を同時に開く」場面で便利です。1行が長くなるときは、\で改行するか、全体を丸かっこで囲んで複数行に分けられます。

主なポイントまとめ

withとコンテキストマネージャの要点をまとめます。

項目 ポイント
役割 後始末(クローズ・解放)を自動化
開始 / 終了 __enter__ / __exit__
asの値 __enter__の戻り値
例外時 __exit__は必ず実行される
簡単に作る @contextmanageryield
複数 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回だけです。前が開始、後が後始末です。

よくある質問

Qwith文は何のためにありますか?
Aファイルや接続などのリソースを使い終わったあとの後始末(クローズや解放)を、自動で確実に行うためです。with open(...) as f:と書くと、ブロックを抜けるときに自動でファイルが閉じられ、エラーが起きても閉じ忘れません。
Qコンテキストマネージャを自作するには?
A__enter__(開始時の処理)と__exit__(終了時の後始末)の2つのメソッドを持つクラスを作ります。__enter__の戻り値がasの変数に入ります。より簡単には、@contextlib.contextmanagerデコレータとyieldを使って関数1つで作れます。
Qブロック内でエラーが起きたら後始末はどうなりますか?
A__exit__は、ブロック内で例外が起きても必ず実行されます。__exit__の引数exc_typeなどに例外の情報が入るため、エラーかどうかを判断して後始末できます。この保証があるため、withはエラー時でもファイルを確実に閉じられます。
Q__exit__がTrueを返すとどうなりますか?
Aブロック内で起きた例外が抑制(握りつぶし)されます。実機でも、__exit__Trueを返すと例外がwithの外に伝わらず、後の処理が実行されました。通常はFalse(または何も返さない)にして、例外はそのまま伝えるのが基本です。
Q複数のファイルを同時にwithで開けますか?
Aできます。with open("a") as f1, open("b") as f2:のようにカンマで区切って並べます。どちらも自動的に閉じられ、終了時は開いた逆の順番で後始末されます。1行が長いときは、全体を丸かっこで囲んで複数行に分けられます。

まとめ

  • with後始末を自動化with open(...) as f:が代表例です。
  • 自作は__enter__(開始)と__exit__(終了)を持つクラスで。
  • asには__enter__の戻り値が入ります。
  • 例外が起きても__exit__は必ず実行され、確実に後始末できます。
  • 簡単に作るなら@contextmanageryield。複数はwith A() as a, B() as b:

コンテキストマネージャは、「使ったら必ず後始末する」処理を安全に書くためのPythonの仕組みです。ファイルや接続、ロックなど、確実に解放したいリソースはwithで管理するのが鉄則です。__enter____exit__の役割と「例外時も後始末される」性質を押さえれば、自分のクラスにもこの安全な仕組みを取り入れられます。