LoginSignup
42
46

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-01-02

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__が最後に呼び出されます。

sample1.py

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

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

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


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

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

前処理
本処理
後処理

as節を使う場合

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

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

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文の入口と出口で暗黙的に呼び出されるので、呼び出し忘れによる事故を防ぎます。

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__が常に呼び出されることが保証されています。

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にします。
以下のコードを実行すると例外は伝搬されずにプログラムは正常終了します。

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()

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

参考

42
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
46