自作したクラスをwith文で安全に使いたい

Pythonにはwith文に対応するための特殊メソッドが用意されています。
2つの特殊メソッドを自作クラスに実装するだけで、自作クラスをwith文で使用することができます。

# TL;DR
- with文を使うことで前処理/後処理の実行漏れを防ぐことができる
- \_\_enter\_\_と\_\_exit\_\_メソッドを実装することでwith文で自作クラスが使えるようになる
- as節を使うには\_\_enter\_\_で自分のインスタンスを返してあげる必要がある
- \_\_enter\_\_の呼び出しが成功した場合は\_\_exit\_\_が常に呼び出されることが保証されている
- 例外発生時に\_\_exit\_\_からTrueを返すと例外の伝搬が中止される

# なぜwith文を使いたいのか
with文は前処理と後処理が暗黙的に呼び出されるので、適切に前処理/後処理を定義さえすれば、自作クラス利用時の前処理/後処理の実行漏れによる事故を防ぐことができます。
一般に、try文と比較して、ファイルやネットワークIO周りの処理を安全かつ簡潔に書くことができます。


# with文に対応させてみる
基本は\_\_enter\_\_と\_\_exit\_\_の特殊メソッドを自作のクラスに実装するだけです。
\_\_enter\_\_が最初に呼び出され、\_\_exit\_\_が最後に呼び出されます。

```python3:sample1.py

class MyClass(object):
    """ 自作のクラス """

    def __enter__(self):
        print("前処理")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("後処理")


with MyClass():
    print("本処理")

```
上のコードを実行すると下記の結果が得られ、暗黙的に前処理と後処理が行われているのが分かります。
また、期待する順番で処理が行われていることも分かります。

```text
前処理
本処理
後処理
```

# as節を使う場合
with文とよく一緒に用いられるものにas節があります。
as節を使うと前処理されたオブジェクトを本処理(withブロック内)で使用することができます。

sample1.pyをas節を使ったコードに書き直してみます。
このコードを実行するとsample1.pyと同じ結果が得られます。

```python3:sample2.py

class MyClass(object):
    """ 自作のクラス """

    def __enter__(self):
        print("前処理")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("後処理")

    def do_something(self):
        print("本処理")


with MyClass() as my_class:
    my_class.do_something()

```
sample1.pyのMyClassとの本質的な違いは、\_\_enter\_\_メソッドでオブジェクトを返しているところです。
ここではself、つまりMyClassのインスタンスを返しています。
\_\_enter\_\_で返したオブジェクトはas節で指定した変数を参照することで使用することができます。
ここではmy_classがas節で指定された変数にあたります。

以降のサンプルコードではこのas節を使います。

# with文の使いどころ

実際のwith文の使いどころとしては前処理/後処理が必要な場面です。
よく使われる場面としてファイルやコネクションのopen/closeがあります。

sample3.pyではこれまでの例よりも少し実用的な例として、CSVファイルの読み込みを行います。
前処理であるファイルのopenを\_\_enter\_\_メソッドで行い、後処理のファイルのcloseを\_\_exit\_\_メソッドで行います。
これらのメソッドはwith文の入口と出口で暗黙的に呼び出されるので、呼び出し忘れによる事故を防ぎます。



```python3:sample3.py

class CsvReader(object):
    """ CSVファイルを読み込むためのクラス """

    def __init__(self, filename):
        self._filename = filename
        self._csv = None

    def __enter__(self):
        self.open()
        return self
   
    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

    def open(self):
        self._csv = open(self._filename, "r")

    def close(self):
        self._csv.close()

    def readline(self):
        readline = self._csv.readline().strip()
        return readline.split(",") if readline else None


with CsvReader("sample.csv") as csv_reader:
    print(csv_reader.readline())
        
```
サンプルコードなので簡単のため処理が雑になっていますが、雰囲気は伝わると思います。
ちなみに、**PythonにはCSVファイルを扱うライブラリが標準ライブラリにあるので、そちらをご利用ください。**

# エラー処理 ※重要
ブロックを抜ける際に\_\_exit\_\_を呼び出して後処理をしてくれるwith文ですが、
\_\_exit\_\_の呼び出しは\_\_enter\_\_の呼び出し成功が前提となっているので注意が必要です。
**つまり\_\_enter\_\_の呼び出しが失敗した場合(\=例外が発生した場合)は\_\_exit\_\_は実行されません。**
例外はそのまま呼び出し元へと伝搬します。
一方で、\_\_enter\_\_の呼び出しに成功した場合(\=例外が発生しなかった場合)は\_\_exit\_\_が常に呼び出されることが保証されています。


```python3:sample4-1.py

class MyClass(object):

    def __init__(self):
        # ここで例外が発生した場合はそもそも__enter__が呼び出されないので__exit__は実行されない
        print("初期化")

    def __enter__(self):
        # ここで例外が発生した場合も__exit__は実行されない
        print("前処理")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("後処理")

    def do_something(self):
        # __enter__の呼び出しが成功しているので、ここでの例外発生の有無に関わらず__exit__が常に実行される
        print("本処理")


with MyClass() as my_class:
    my_class.do_something()

```

\_\_enter\_\_の呼び出しが成功した場合withブロック内での例外発生の有無に関わらず\_\_exit\_\_が常に呼び出されますが、例外発生の有無によって呼び出し時に渡される引数が異なります。
例外が発生した場合の\_\_exit\_\_の引数はselfを除いて左から順番に、発生した例外のクラス、メッセージ、tracebackオブジェクトです。また、例外が発生しなかった場合はすべての引数にNoneが渡されます。

使う機会は少ないと思いますが、例外発生時に\_\_exit\_\_で例外の伝搬を止めることができます。
伝搬を止めるには\_\_exit\_\_の返り値をTrueにします。
以下のコードを実行すると例外は伝搬されずにプログラムは正常終了します。

```python3:sample4-2.py

class MyClass(object):

    def __enter__(self):
        print("前処理")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("後処理")
        # Trueを返すことで例外の伝搬を止めることができる
        return True

    def do_something(self):
        print("本処理")
        raise Exception("例外発生")


with MyClass() as my_class:
    my_class.do_something()

```
例外の伝搬を止めることはできますが、意図せず例外を握りつぶしてしまわないよう注意が必要です。



# 参考
- [Python言語リファレンス 8.5 with文](https://docs.python.jp/3/reference/compound_stmts.html#with)
- [Python言語リファレンス 3.3.8 with文とコンテキストマネージャ](https://docs.python.jp/3/reference/datamodel.html#context-managers)