Python withの使い方

# withとは

最初にwithの具体的な例を挙げます。 たとえば、ファイルを開けたら責任をもってcloseしないといけません。

withを使わない場合の例
```python
f = open("example.txt", "r")

print(f.read())

# 自分で閉じないといけない
f.close()
```

withを使う場合の例
```python
# withを使うとブロックを抜けたときに閉じてくれる
with open("example.txt", "r") as f:
    print(f.read())
```

このように、ブロックに入るとき、出るときに特定の処理を自動的に実行させる方法を解説します。


# 実際に作ってみる

公式ドキュメントを参考にしています。

https://docs.python.org/ja/3.11/library/contextlib.html?highlight=with
## クラスの場合
関数と比べて可読性が良く、分かりやすいです。

```python
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)

これらの引数を活用して例外処理を行うことができます：

```python
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文の進行について　
引用：以下リンク

https://docs.python.org/ja/3.11/reference/compound_stmts.html#grammar-token-python-grammar-with_item

>一つの "要素" を持つ with 文の実行は以下のように進行します:
>
>コンテキスト式 (with_item で与えられた式) を評価することで、コンテキストマネージャを取得します。
>
>コンテキストマネージャの __enter__() メソッドが、後で使うためにロードされます。
>
>コンテキストマネージャの __exit__() メソッドが、後で使うためにロードされます。
>
>コンテキストマネージャの __enter__() メソッドが呼ばれます。
>
>with 文にターゲットが含まれていたら、それに __enter__() からの戻り値が代入されます。


### asを使うには
```python
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`は実装する必要があります。
```python
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
```

## 関数で値を返す場合

```python
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`デコレータはジェネレータ関数をコンテキストマネージャに変換します：

1. withブロックに入ると、ジェネレータが実行され`yield`まで処理が進みます
2. `yield`の値がwithブロックに渡されます（as変数に代入）
3. withブロック内の処理が実行されます
4. withブロックを抜けると、ジェネレータ内の`yield`以降の処理が再開されます
5. 例外が発生した場合は`except`ブロックでキャッチできます
6. `finally`ブロックは必ず実行されます。