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__が最後に呼び出されます。
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と同じ結果が得られます。
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文の入口と出口で暗黙的に呼び出されるので、呼び出し忘れによる事故を防ぎます。
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__が常に呼び出されることが保証されています。
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にします。
以下のコードを実行すると例外は伝搬されずにプログラムは正常終了します。
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()
例外の伝搬を止めることはできますが、意図せず例外を握りつぶしてしまわないよう注意が必要です。