with句の一般用途
主にテキストファイルを読み書きする際なんかに使用されているのが一番イメージしやすいかなと思います。
with open("hoge.txt","r") as f :
for line in f :
print(line)
簡単に言ってしまえば 「後片付け」 と 「エラー処理」 を勝手にやってくれる便利機能です。
もちろん文法として用意されているものですから open()
以外にもいろんな処理で使用することができます。
with句が利用できる処理
クラスの中に __enter__()
と __exit__()
を持っているオブジェクトで利用可能です。
名前からある程度推察できるかと思いますが、with句
が呼び出されたときに__enter__()
が実行され、終了したときに__exit__()
が実行されるという仕組みです。
試しに__enter__()
と__exit__()
が存在しない関数でwith句
を使用してみるとエラーになります。
class Hoge():
def __init__(self):
pass
def execute(self):
print("hoge")
with Hoge() as hoge:
hoge.execute()
TypeError: 'Hoge' object does not support the context manager protocol
正常に動くパターンは後述します。
with句の中身
具体例としてThreadPoolExecutor
を見ていきます。
以下の通りThreadPoolExecutor
は __enter__()
と __exit__()
を持っていることを確認できます。
from concurrent.futures import ThreadPoolExecutor
print(dir(ThreadPoolExecutor))
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_adjust_thread_count', '_counter', '_initializer_failed', 'map', 'shutdown', 'submit']
__enter__()
とは
with句の中で使用するオブジェクトを返却します。
返却されたオブジェクトはas
の後に定義された変数に格納されます。
※ with open("hoge.txt","r") as f :
でいうf
の部分
返却するオブジェクトは以下の2通りがあります。
1. 自分自身(self)
2. 関連する別オブジェクト
ThreadPoolExecutor
(正確には親クラスのExecutor
)ではself
を返却しています。(open()
も同様です。)
def __enter__(self):
return self
関連する別オブジェクトを返却するパターンの代表例としてはdecimal.localcontext
が挙げられます。
クラス自体は_ContextManager
ですが、__enter__()
が返却しているのはContext
型のオブジェクトを返却していますね。
class _ContextManager:
new_context: Context
saved_context: Context
def __init__(self, new_context: Context) -> None: ...
def __enter__(self) -> Context: ...
def __exit__(self, t: type[BaseException] | None, v: BaseException | None, tb: TracebackType | None) -> None: ...
__exit__()
とは
with句を抜ける際に実行される処理です。異常終了の場合も実行されます。
そういう点ではfinally
的な位置づけだと思ってください。
ThreadPoolExecutor
の場合はshutdown()
を呼び出した後でFalse
をリターンしています。
def __exit__(self, exc_type, exc_val, exc_tb):
self.shutdown(wait=True)
return False
冒頭で記述した 「後片付け」 の処理がThreadPoolExecutor
の場合はshutdown()
ということですね。
また、この関数には引数と戻り値があります。
引数について
本関数は3つの引数を受け取れるように実装しなければなりません。 第一引数 : 例外の型
第二引数 : 例外の値
第三引数 : トレースバックオブジェクト
正常終了の場合は、各引数にNone
が設定されます。
def __exit__(self, exc_type, exc_val, exc_tb):
print("exc_type : ", exc_type)
print("exc_val : ", exc_val)
print("exc_tb : ")
import traceback
traceback.print_tb(exc_tb)
exc_type : <class 'TypeError'>
exc_val : HogeによるTypeErrorが発生しました
exc_tb :
File "with.py", line 24, in <module>
raise TypeError("HogeによるTypeErrorが発生しました")
戻り値について
ThreadPoolExecutor
の__exit__()
にもある通り、本関数はBoolean
を返却します。
(省略された場合はFalse
の挙動になります)
これはwith句
の中でエラーが発生した時の挙動に影響するもので、True
が返却された場合は発生したエラーを握りつぶしてwith句
終了後から後続処理を続行します。
実例
上記の内容を踏まえて自作のクラスを作ってみます。
正常系
class Hoge():
def __init__(self):
print(1)
def __enter__(self):
print(2)
return self
def execute(self):
print(4)
print("hoge")
def __exit__(self, exc_type, exc_val, exc_tb):
print(6)
with Hoge() as hoge:
print(3)
hoge.execute()
print(5)
print(7)
1
2
3
4
hoge
5
6
7
実行結果からわかるように
1. __init__()
の実行
2. __enter__()
の実行
3. with句
の中身を実行
4. with句
が終了するタイミングで__exit__()
の実行
5. with句
外の後続処理を実行
という順番で動いています。
例外発生時
class Hoge():
def __init__(self):
print(1)
def __enter__(self):
print(2)
return self
def execute(self):
print(4)
print("hoge")
def __exit__(self, exc_type, exc_val, exc_tb):
print(6)
+ return False
with Hoge() as hoge:
print(3)
+ raise TypeError
hoge.execute()
print(5)
print(7)
1
2
3
6
Traceback (most recent call last):
File "with.py", line 20, in <module>
raise TypeError
TypeError
例外発生以降の後続処理は行われずエラー終了しますが、__exit__()
の処理は実行されているのがわかります。
例外を握りつぶす場合
class Hoge():
def __init__(self):
print(1)
def __enter__(self):
print(2)
return self
def execute(self):
print(4)
print("hoge")
def __exit__(self, exc_type, exc_val, exc_tb):
print(6)
- return False
+ return True
with Hoge() as hoge:
print(3)
raise TypeError
hoge.execute()
print(5)
print(7)
1
2
3
6
7
例外発生以降の処理は実行されていませんがエラーは出力されず、with句
の後から処理が再開しているのがわかります。
参考資料
上記記事は公式のリファレンスを元に作成しました。