Pythonにおけるwith構文の挙動を簡単に確認する

with構文の使い方というか実装の仕方を忘れがちなのでメモがてら簡単な確認コードを書いておく.

class Test(object):

    def __init__(self, *args, **kwargs):
        print('__init__', args, kwargs)

    def __enter__(self, *args, **kwargs):
        print('__enter__', args, kwargs)
        return self  #XXX: Donot forget this

    def __exit__(self, *args, **kwargs):
        print('__exit__', args, kwargs)

    def __del__(self, *args, **kwargs):
        print('__del__', args, kwargs)

    def say(self):
        print('hello')


if __name__ == '__main__':
    Test('spam').say()
    print('end1')

    with Test('spam') as obj:
        obj.say()
    print('end2')

これを実行すると、

('__init__', ('spam',), {})
hello
('__del__', (), {})
end1
('__init__', ('spam',), {})
('__enter__', (), {})
hello
('__exit__', (None, None, None), {})
end2
('__del__', (), {})

のようになるはず. 注意すべきは最後の__del__がend2の後にあることか. この実装だと二度目のTestはコードの最後までdelされない. __exit__でdel selfとしても無駄である. __enter__でreturnするオブジェクトがself以外であった場合(return Test()などとしてみれば良い), そのオブジェクトはwith構文のスコープを抜けるときにdelされるようだが, Testそのものはやはりdelされない.

ちなみに本業wのwith文中で例外が発生した場合を試すためには,

with Test('spam') as obj:
    obj.say()
    raise RuntimeError('oops!')
print('end1')

のようにしてみれば良くて, 結果はちゃんと以下のようになっている. end1は当然呼ばれない.

('__init__', ('spam',), {})
('__enter__', (), {})
hello
('__exit__', (<type 'exceptions.RuntimeError'>, RuntimeError('oops!',), <traceback object at 0x7fd14ccffc68>), {})
Traceback (most recent call last):
  File "with_statement.py", line 30, in <module>
    raise RuntimeError('oops!')
RuntimeError: oops!
('__del__', (), {})

http://docs.python.jp/3/reference/compound_stmts.html#with