【Python】with句の中で動いている処理

# with句の一般用途
主にテキストファイルを読み書きする際なんかに使用されているのが一番イメージしやすいかなと思います。

```python
with open("hoge.txt","r") as f :
    for line in f :
        print(line)
```

簡単に言ってしまえば　「後片付け」　と　「エラー処理」　を勝手にやってくれる便利機能です。
もちろん文法として用意されているものですから　`open()`　以外にもいろんな処理で使用することができます。

# with句が利用できる処理
クラスの中に `__enter__()`　と　`__exit__()`　を持っているオブジェクトで利用可能です。
名前からある程度推察できるかと思いますが、`with句`が呼び出されたときに`__enter__()`が実行され、終了したときに`__exit__()`が実行されるという仕組みです。

試しに`__enter__()`と`__exit__()`が存在しない関数で`with句`を使用してみるとエラーになります。
```python
class Hoge():
    def __init__(self):
        pass
    def execute(self):
        print("hoge")

with Hoge() as hoge:
    hoge.execute()
```
```console
TypeError: 'Hoge' object does not support the context manager protocol
```

正常に動くパターンは後述します。

# with句の中身
具体例として`ThreadPoolExecutor`を見ていきます。
以下の通り`ThreadPoolExecutor`は　`__enter__()`　と　`__exit__()`　を持っていることを確認できます。
```python
from concurrent.futures import ThreadPoolExecutor
print(dir(ThreadPoolExecutor))
```
```console
['__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()`も同様です。）
```python
def __enter__(self):
    return self
```

関連する別オブジェクトを返却するパターンの代表例としては`decimal.localcontext`が挙げられます。
クラス自体は`_ContextManager`ですが、`__enter__()`が返却しているのは`Context`型のオブジェクトを返却していますね。
```_decimal.pyi
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`をリターンしています。
```python
def __exit__(self, exc_type, exc_val, exc_tb):
    self.shutdown(wait=True)
    return False
```
冒頭で記述した　「後片付け」　の処理が`ThreadPoolExecutor`の場合は`shutdown()`ということですね。

また、この関数には引数と戻り値があります。

:::note info

<details><summary>引数について</summary>
本関数は3つの引数を受け取れるように実装しなければなりません。

　第一引数　：　例外の型
　第二引数　：　例外の値
　第三引数　：　トレースバックオブジェクト

正常終了の場合は、各引数に`None`が設定されます。

```python
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)
```
```console
exc_type :  <class 'TypeError'>
exc_val  :  HogeによるTypeErrorが発生しました
exc_tb   :
  File "with.py", line 24, in <module>
    raise TypeError("HogeによるTypeErrorが発生しました")
```
</details>
:::

:::note info

<details><summary>戻り値について</summary>

`ThreadPoolExecutor`の`__exit__()`にもある通り、本関数は`Boolean`を返却します。
（省略された場合は`False`の挙動になります）

これは`with句`の中でエラーが発生した時の挙動に影響するもので、`True`が返却された場合は発生したエラーを握りつぶして`with句`終了後から後続処理を続行します。
</details>
:::

# 実例
上記の内容を踏まえて自作のクラスを作ってみます。

## 正常系

```python
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)
```
```console
1
2
3
4
hoge
5
6
7
```
実行結果からわかるように
　1.　`__init__()`の実行
　2.　`__enter__()`の実行
　3.　`with句`の中身を実行
　4.　`with句`が終了するタイミングで`__exit__()`の実行
　5.　`with句`外の後続処理を実行
という順番で動いています。

## 例外発生時
```diff_Python
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)
```
```console
1
2
3
6
Traceback (most recent call last):
  File "with.py", line 20, in <module>
    raise TypeError
TypeError
```
例外発生以降の後続処理は行われずエラー終了しますが、`__exit__()`の処理は実行されているのがわかります。

## 例外を握りつぶす場合
```diff_python
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)
```
```console
1
2
3
6
7
```
例外発生以降の処理は実行されていませんがエラーは出力されず、`with句`の後から処理が再開しているのがわかります。


# 参考資料
上記記事は公式のリファレンスを元に作成しました。

https://docs.python.org/ja/3.13/reference/compound_stmts.html#the-with-statement

https://docs.python.org/ja/3.13/reference/datamodel.html#with-statement-context-managers

https://docs.python.org/ja/3.13/library/stdtypes.html#context-manager-types