withとは
最初にwithの具体的な例を挙げます。 たとえば、ファイルを開けたら責任をもってcloseしないといけません。
withを使わない場合の例
f = open("example.txt", "r")
print(f.read())
# 自分で閉じないといけない
f.close()
withを使う場合の例
# withを使うとブロックを抜けたときに閉じてくれる
with open("example.txt", "r") as f:
print(f.read())
このように、ブロックに入るとき、出るときに特定の処理を自動的に実行させる方法を解説します。
実際に作ってみる
公式ドキュメントを参考にしています。
クラスの場合
関数と比べて可読性が良く、分かりやすいです。
class Context:
def __enter__(self):
print("enter")
return self # 必要に応じてオブジェクトを返す
def __exit__(self, exc_type, exc_val, exc_tb):
print("exit")
if __name__ == "__main__":
with Context():
print("body")
実行結果
enter
body
exit
解説
__enter__
メソッドはブロックに入る前に実行される処理を定義します。
__exit__
メソッドはwithブロックを抜けたときの処理を定義します。
__exit__
メソッドの引数について
__exit__
メソッドは以下の3つの引数を受け取ります:
-
exc_type
: 発生した例外の型(例外がなければNone) -
exc_val
: 例外のインスタンス(例外がなければNone) -
exc_tb
: トレースバック情報(例外がなければNone)
これらの引数を活用して例外処理を行うことができます:
class SafeContext:
def __enter__(self):
print("リソースを取得します")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
print(f"例外が発生しました: {exc_type.__name__} - {exc_val}")
# 例外を処理する場合はTrueを返す(例外が呼び出し元に伝播しなくなる)
return True
print("正常終了:リソースを解放します")
return False # 例外があれば再送出(デフォルト)
if __name__ == "__main__":
# 正常処理の場合
with SafeContext():
print("正常処理")
# 例外発生の場合
with SafeContext():
print("例外を発生させます")
raise ValueError("テスト例外")
print("プログラムは続行されます") # 例外が処理されたので実行される
with文の進行について
引用:以下リンク
一つの "要素" を持つ with 文の実行は以下のように進行します:
コンテキスト式 (with_item で与えられた式) を評価することで、コンテキストマネージャを取得します。
コンテキストマネージャの enter() メソッドが、後で使うためにロードされます。
コンテキストマネージャの exit() メソッドが、後で使うためにロードされます。
コンテキストマネージャの enter() メソッドが呼ばれます。
with 文にターゲットが含まれていたら、それに enter() からの戻り値が代入されます。
asを使うには
class FileContext:
def __init__(self, file_name: str, mode):
self.f = open(file_name, mode)
def __enter__(self):
return self.f
def __exit__(self, exc_type, exc_val, exc_tb):
self.f.close()
# 例外が発生した場合のログ記録などもここで行える
if exc_type:
print(f"ファイル操作中に例外が発生: {exc_val}")
if __name__ == "__main__":
with FileContext("example.txt", "r") as f:
print(f.read())
__enter__
メソッドの戻り値がasで指定した変数に代入されます。これにより、withブロック内でその値を使用できます。
実用例として考えられるのは:
- データベース接続の自動クローズ
- ファイルの自動クローズ
- スレッドロックの自動解放
- 一時的な設定変更と元に戻す処理
- 計測処理(処理時間の計測など)
関数の場合
クラスを使う場合と比べて、少し面倒だと感じます。
最低限必要なコード
最低限try
とfinally
そしてyield
は実装する必要があります。
from contextlib import contextmanager
# デコレーターを使う
@contextmanager
def WithContext():
print("enter") # 前処理
try:
yield # withブロック内の処理へ制御を移す
finally:
print("exit") # 後処理(必ず実行される)
if __name__ == "__main__":
with WithContext():
print("body")
実行結果
try
Context
finally
関数で値を返す場合
from contextlib import contextmanager
@contextmanager
def file_opener(filename, mode="r"):
try:
f = open(filename, mode)
yield f # yieldした値がasの後ろの変数に代入される
except Exception as e:
print(f"ファイル操作中にエラーが発生: {e}")
raise # 例外を再送出
finally:
print("ファイルを閉じます")
f.close() # 例外があってもなくても必ず実行
if __name__ == "__main__":
with file_opener("example.txt") as f:
print(f.read())
yield
を使うことで、withブロック内へ値を渡し、ブロックの実行が終わったら処理を再開します。例外処理もtry-except-finally
構文で自然に書けます。
コンテキストマネージャの内部動作
@contextmanager
デコレータはジェネレータ関数をコンテキストマネージャに変換します:
- withブロックに入ると、ジェネレータが実行され
yield
まで処理が進みます -
yield
の値がwithブロックに渡されます(as変数に代入) - withブロック内の処理が実行されます
- withブロックを抜けると、ジェネレータ内の
yield
以降の処理が再開されます - 例外が発生した場合は
except
ブロックでキャッチできます -
finally
ブロックは必ず実行されます。