年中アイス

いろいろつらつら

with構文とは何なのか

Pythonをやっていて、with構文って何だろ?となって、理解できたので整理してみます。細かいところが違うかもしれませんが、動きはつかめるかなと思います。
Python3.3で動作確認しています。

with構文とは

with構文は、ある機能の利用者が、より安全、簡潔にその機能を使えるようにする構文です。既知の定形終了処理であれば、機能作成側でそれをあらかじめ定義し、利用者はwith構文を使うだけで、安全に機能を使うことが出来ます。

with構文を使っていないファイル書き込み例

wfp = open('msg.log', 'w')
wfp.write('need call close, if do not use with statement.')
wfp.close()

with構文を使ったファイル書き込み例

with open('msg.log', 'w') as wfp:
    wfp.write('it is example for with statement.')
    wfp.write('do not need call close().')

with構文を使うと、close()の呼び出しが不要です。withブロックを抜けると、自動でclose()を呼び出してくれます。withブロック内で例外が起きた場合も同様です。ただし、withの行で例外が発生した場合は、close()は呼ばれません。

この仕組みを、実際にwith構文で使えるクラスを作成しながら確認します。

with構文の動き

with構文で使えるクラスには、2つのメソッドが実装されています。

  • __enter__(self)
  • __exit__(self, exception_type, exception_value, traceback)

この2つを実装したクラスをwithに渡すと次のタイミングで呼ばれます。*1

with # call __enter__() as var_name:
    Your logic
    # call __exit__()

呼ばれる順序を、次のクラスを実行して確認して行きます。

Example Python with statement 2013/8/24 fix LABEL comment at main(). LABEL was different to explain on blog. http://d.hatena.ne.jp/reiki4040/20130331/1364723288
※なぜかgist埋め込みがプレビューされないので、リンクで。

まずは、正常に終了する場合。

$ python ExampleWithClass.py

--before with statement--
get_instance()
__init__
__enter__
do with block.
print from ExampleWithClass.
__exit__
  close()
  exception_type: None
  exception_value: None
  traceback: None
--after with statement--
__del__

__enter__, __exit__がwithブロックの開始と終了時に呼ばれていることがわかります。*2

このコードの、printの後に、raiseを入れて例外を起こします。main()内の[LABEL A]の行頭の#を削除して実行します。

$ python ExampleWithClass.py

--before with statement--
__init__
__enter__
do with block.
__exit__
  close()
  exception_type: <class 'ValueError'>
  exception_value: 
  traceback: <traceback object at 0x10d21ef80>
catch exception
--after with statement--
__del__

そうすると例外によって中断され、print from ExampleWithClassは表示されませんが、__exit__()は呼ばれていることがわかります。

また、__exit__()の戻り値によって、例外を握りつぶすかが決まります。Falseを返せば伝搬され、Trueを返すと握りつぶされます。サンプルコードでは、デフォルトでFalseが返されているため、外側のtry-exceptで、例外が補足され、catch exceptionが出力されています。

Trueを返すようにすると、例外が伝搬されないため、exceptに入らず、このメッセージは出力されなくなります。__exit__()内の[LABEL B]2行の行頭の#を削除して実行します。

$ python ExampleWithClass.py

--before with statement--
__init__
__enter__
do with block.
__exit__
  close()
  exception_type: <class 'ValueError'>
  exception_value: 
  traceback: <traceback object at 0x10ed00f80>
--after with statement--
__del__

例外が握りつぶされたため、catch exceptionが出力されていません。

次に、__enter__()から例外が発生するケース。前述の[LABEL A,B]の行頭に再び#を入れて、__enter__()内の[LABEL C]の行頭の#を削除して実行します。

$ python ExampleWithClass.py

--before with statement--
__init__
__enter__
catch exception
__del__
--after with statement--

__enter__()から例外が出た場合は、__exit__()は呼ばれません。

with構文用のクラスの作り方

最後に、with構文で使えるクラスを作るための、__enter__()と、__exit__()の詳細です。

__enter__()

__enter__()は、これらのメソッドの実装されたクラスのインスタンスを返します。ここで返されるインスタンスが、with ~ as var_nameのvar_nameに入り、withブロック内で使えます。

__exit__()

__exit__()では、規定の終了処理を行います。また、例外が発生した場合の処理もここに記述します。__exit__()の引数は以下です。*3

exception_type 例外のインスタンス
exception_value 例外のメッセージ???(よくわからず)
traceback 例外のトレースバックインスタンス

正常にwithブロック内の処理が終了すると、この3つの引数には、すべてNoneが入ります。例外発生時は、それぞれに例外に対応した値が渡されます。

I/Oのように、closeといった終了処理が必要な場合は、発生する例外に関係なく、close処理をします。もし、特定の例外に対応した処理が必要な場合は、この引数をもとにifなどで分岐して処理します。

その後、何事も無かったかのように扱うか、ユーザに通知するかを戻り値で決めます。伝搬の要否をbooleanで返します。何も書かなければ、デフォルトでTrueが返ります。

Trueなら、発生した例外がそのまま伝搬され、外側のtry-exceptで処理されます。Falseなら、例外は握りつぶされます。ぱっと状況が思い浮かびませんが、何か対応が決まっており、それを通知しなくてもよい場合(通知されても利用者が対応しようがないもの)は、Falseを返すのかなと思います。

所感

with構文は、コードの品質も上げやすく、コードもすっきりするのでよいですね。ファイル操作にwithを使っていなかったので、今度から使おうと思います。Javaで、finally使ってclose()していないのと同じぐらい、コードとしてはおかしいのかな。

参考

このあたりを参考にさせていただきました。あとは実行確認。
PY習 with文
Understanding Python's "with" statement

実際にライブラリコードで利用している訳ではないので、何か違うところや、いいサンプルがあれば、コメントなどで教えてください。

*1:enterは、たぶんここ?

*2:__init__(),__del__()は、インスタンス生成時、破棄時に呼ばれるメソッドです。

*3:引数の名前は、t,v,tbなど、変えても大丈夫です。