matobaの備忘録

育児しながら働くあるエンジニアの記録

Pythonのコンテキストマネージャってなんなの?と思って調べた話

Pythonのコンテキストマネージャって何?って言われると、「withを使ってるあれ」みたいな答え方はできるんですが、以下のような質問は答えられませんでした。というわけで、調べたことでも書きます。

  • コンテキストマネージャって、なんのためにあるの?
  • コンテキストマネージャって、どうやって実装するの?
  • コンテキストマネージャって、何が必要なの?

コンテキストマネージャって何?

公式ドキュメントを見ると以下のように書いています。

3. データモデル — Python 3.6.5 ドキュメント

コンテキストマネージャ(context manager) とは、 with 文の実行時にランタイムコンテキストを定義するオブジェクトです。

なるほど、よくわかりません。with 文の実行時にランタイムコンテキストってなんなの?と思います。

with文ってそもそも何?

with文についてもう少し調べます。 公式ドキュメントでは、withの説明はこちらにあります。

8. 複合文 (compound statement) — Python 3.6.5 ドキュメント

with 文は、ブロックの実行を、コンテキストマネージャによって定義されたメソッドでラップするために使われます (with文とコンテキストマネージャ セクションを参照してください)。

なるほど?とりあえず、コンテキストマネージャを理解するためには、with文を理解する必要があって、with文を理解するためには、コンテキストマネージャを理解する必要があることがわかりました。

これは難しい。

with文の成り立ちを見る

Pythonのドキュメントだけでは、理解することができなかったので、次は、PEPを読みます。

withのPEPがありました。

www.python.org

最初の方に以下の文があります。

This PEP adds a new statement "with" to the Python language to make it possible to factor out standard uses of try/finally statements.

ああ、毎回同じようなtry/finallyを書くのを止めるために、withを導入した、という背景があるんですね。

そして、次にこんな風に書いてあります。

In this PEP, context managers provide __enter__() and __exit__() methods that are invoked on entry to and exit from the body of the with statement.

コンテキストマネージャーは、__enter__() __exit__() メソッドを実行するんですね。

なるほど

コンテキストマネージャを雑に実装して見る

とりあえず、なんでも良いので、コンテキストマネージャを実装してみます。

class context_manager_sample:
    def __enter__(self, *args):
        print('enter')

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

with context_manager_sample() as f:
    print('hello context manager')

これを実行してみます。

$ python3 context_manager_sample.py
enter
hello context manager
exit

なんか動いたようです。

エラーを起こして見る

try/finarlly に置き換えるものなので、コンテキストマネージャーの中でエラーが起きてもexitは実行されるんでしょう。

やってみます。

class context_manager_sample:
    def __enter__(self, *args):
        print('enter')

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

with context_manager_sample() as f:
    print('hello context manager')
    raise Exception('cause error!')
$ python3 context_manager_sample.py
enter
hello context manager
exit
Traceback (most recent call last):
  File "context_manager_sample.py", line 10, in <module>
    raise Exception('cause error!')
Exception: cause error!

確かに、exitが処理されています。 なるほど。

contextlibを使う

Pythonでは、標準ライブラリとして、contextlibが提供されていました。

29.6. contextlib — with 文コンテキスト用ユーティリティ — Python 3.6.5 ドキュメント

先ほどのように、自分で __enter__ __exit__ を実装するのは、悪手のように見えます。

次はこちらを使って同じような処理を実装してみます。

from contextlib import contextmanager

@contextmanager
def context_manager_sample():
    print('enter method')
    yield
    print('exit method')


with context_manager_sample():
    print('hello context manager')
$ python3 contextlib_sample.py
enter method
hello context manager
exit method

ふむ。こちらの方が便利そうですね。

ちょっと待って、asは?

そういえば、withで指定した際のasってどうなってるんだろう、

と思って調べたら、yieldに入ってる値がasに入るらしいですね

from contextlib import contextmanager

@contextmanager
def context_manager_sample():
    print('enter method')
    yield 'yield value'
    print('exit method')


with context_manager_sample() as sample:
    print(f'hello context manager: {sample}')
$ python3 contextlib_sample2.py
enter method
hello context manager: yield value
exit method

こういう感じ。

へえー

追記

エキスパートPythonプログラミング改訂2版を読んでると、コンテキストマネージャについて、もう少し詳しく書いてました。

エキスパートPythonプログラミング改訂2版