Python 3.3 からの with 文

以前、Mockライブラリの説明記事 でこんなことを書いた。

コンテキストマネージャで patch() を使う場合、複数のオブジェクトを同時に置き換える時に

def test():
    with patch('testapp.func1') as m1:
        with patch('testapp.func2') as m2:
            with patch('testapp.func3') as m3:
                with patch('testapp.func4') as m4:
                    func()

なんて書くのはみっともない。Python2.xなら、 contextlib.nested() を使ってこう書こう。

from contextlib import nested
def test():
    with nested(
        patch('testapp.func1'),
        patch('testapp.func2'),
        patch('testapp.func3'),
        patch('testapp.func4'))
    as (m1, m2, m3, m4):
        func()

ここで Python2.x なら と書いているが、これは卑怯なごまかしである。実は、Python3 では、contextlib.nested() が廃止されたため、どうにもいまいちな書き方になりがちだったのだ。

Python3 では、`contextlib.nested()` が廃止され、代わりに with 文に複数のコンテキストを指定できるようになった。

def test():
    with patch('testapp.func1') as m1, patch('testapp.func2') as m2:
        func()

これは良いのだが、`with` 文には () が書けないため、たくさんのコンテキストマネージャを with 文に書くのが面倒になってしまった。

本当は、

def test():
    with (patch('testapp.func1') as m1, 
          patch('testapp.func2') as m2,
          patch('testapp.func2') as m3,
          patch('testapp.func2') as m4):

        func()

のように、`()` で全体を囲んで自由に改行出来ればよいのだが、これはシンタックスエラーとなってしまう。上のように書きたければ、行末に \\ をつけて

def test():
    with patch('testapp.func1') as m1, \
         patch('testapp.func2') as m2, \
         patch('testapp.func2') as m3, \
         patch('testapp.func2') as m4:

        func()

とするか、

def test():
    with (patch('testapp.func1')) as m1, (
          patch('testapp.func2')) as m2, (
          patch('testapp.func2')) as m3, (
          patch('testapp.func2')) as m4:

        func()

のように、むりやり変な位置に () を付けるしかなくなってしまった。

「カッコ書けるようにすればいいじゃん」と思うかもしれないが、Python のパーサでこれをやるのは意外と難しい。ここ でも話題になっているが、 普通の式の後ろに as を書けるようにするのは、パーサに手を入れないとうまく行かなさそうだ。そして、パーサにはできるだけ手を入れないというのが Python 界の不文律なのだ。

全体をカッコで囲むのは難しいが、別案としては

def test():
    with (patch('testapp.func1'),
          patch('testapp.func2')
         ) as (
           m1, 
           m2):

        func()

のように、コンテキストマネージャ部分をカッコで囲み、それを受け取る変数名を分離する書き方であれば結構かんたんにできた。しかし、結局は既存の構文と同じ機能でしかないわけで、同じ事をするのに複数の構文を導入するのは意味が無い。

contextlib.ExitStack()

Python 3.3 では、contextmanagernested() に 代わって ExitStack() が導入された。ExitStack() を使えば、こんなかんじでコンテキストマネージャを使用出来る。

from contextlib import ExitStack 
def test():
    with ExitStack() as stack:
        m1 = stack.enter_context(patch('testapp.func1'))
        m2 = stack.enter_context(patch('testapp.func2'))
        m3 = stack.enter_context(patch('testapp.func3'))
        m4 = stack.enter_context(patch('testapp.func4'))

        func()

これならば見た目もすっきりしているし、Python2 の nested() を使ったコードよりも自由度が高い。ということで、`contextlib.ExitStack()` の利用をおすすめする次第である。