🐍

Pythonのwith文の正体

2022/05/31に公開

結論だけ知りたい人は解明編へ。
筆者はPythonistaではないため誤った理解をしている可能性があります。間違いなど見つけましたらご指摘いただけると嬉しいです。

with文との邂逅

Pythonを学び、ファイルの操作方法が分かった頃、初めてwith文というものを知りました。
それ以来、私はいつも当たり前のようにwith文とopen()関数をセットで使っていましたが、とあるコードを読んだ時に、ファイル操作以外に使われているのを発見しました。
そもそもwith文とは何なのか、その謎を解明すべく、我々はアマゾンの奥地へと向かった…

調査編

まずは、公式ドキュメントを読みます。
https://docs.python.org/ja/3/reference/compound_stmts.html#the-with-statement

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

私のエンジニア力不足なのか、書いてあることがよく分かりません。
そもそもコンテキストマネージャとは何なのでしょうか。
「with文とコンテキストマネージャ セクションを参照しろ」とあるので参照します。
https://docs.python.org/ja/3/reference/datamodel.html#context-managers

コンテキストマネージャとは

コンテキストマネージャ(context manager) とは、 with 文の実行時にランタイムコンテキストを定義するオブジェクトです。コンテキストマネージャは、コードブロックを実行するために必要な入り口および出口の処理を扱います。コンテキストマネージャは通常、 with 文( with 文 の章を参照)により起動されますが、これらのメソッドを直接呼び出すことで起動することもできます。

コンテキストマネージャの代表的な使い方としては、様々なグローバル情報の保存および更新、リソースのロックとアンロック、ファイルのオープンとクローズなどが挙げられます。

コンテキストマネージャについてのさらなる情報については、 コンテキストマネージャ型 を参照してください。

with文とコンテキストマネージャは密接に関わり合っているようですが、イマイチ良くわかりません。ただ、最後の1文にコンテキストマネージャ型とあるので、どうやらコンテキストマネージャはPythonの型の1つのようです。
ドキュメントの組み込み型一覧の中にもありました。
https://docs.python.org/ja/3/library/stdtypes.html#context-manager-types

さらに、ユーザーがコンテキストマネージャ型を定義するために必要なメソッドを発見しました。

contextmanager.__enter__()
contextmanager.__exit__()

contextmanager.__enter__()の説明にはこのようにあります。

自分自身を返すコンテキストマネージャの例として ファイルオブジェクト があります。ファイルオブジェクトは __enter__() から自分自身を返し、 open() が with 文のコンテキスト式として使われるようにします。

ファイルオブジェクトはコンテキストマネージャ...?
正直、自分の理解はまだフワッとしていますが、何となく概要は掴めてきました。
要するにコンテキストマネージャとは、__enter()____exit()__を持つPythonの型という事でいいでしょう。

あまり奥に進むと沼にハマってしまいそうなので、次に進みます。

with文とは

それでは、本記事のメインであるwith文の方を掘り進めてみます。
PEP 343に、with文の仕様、背景、および例が記載されていました。
https://peps.python.org/pep-0343/
最初のAbstractの部分で大事なことが書かれていました。

This PEP adds a new statement “with” to the Python language to make it possible to factor out standard uses of try/finally statem

新しくwith文を追加することで、try/finally文を括り出す(factor out)ことができるとあります。

try/finally文とは

try文は例外処理を書くためのもので、except文と共に次のような使われ方が多いです。
try文の中で例外が発生する可能性のあるコードを記述し、except文で例外を補足します。

try:   
  n = 1/0      
except ZeroDivisionError:
  print("ゼロ除算")    

例外処理には、try文とexcept文の他にelse文とfinally文があります。
else文は例外が発生せずに処理が進んだ場合実行されるもので、finally文は例外が発生したかどうかに関わらず、最後に必ず実行されます。
次のコードはドキュメントにあった例です。

>>> def f():
...     try:
...         1/0
...     finally:
...         return 42
...
>>> f()
42

with文とtry/finally文

try/finally文を使ったプログラムでは、最初に何かをして、最後にも必ず何かをします。
この最初と最後に何かをするというのを一括りにしたものがwith文のようです。
PEP 343にwith文の正体が載っていました。

with EXPR as VAR:
    BLOCK

上記のようなコードを、try/finally文で訳すとおおよそ次のようになるようです。

VAR = EXPR
VAR.__enter__()
try:
    BLOCK
finally:
    VAR.__exit__()

ここで出てきました、コンテキストマネージャの__enter__()__exit__()
何となく正体が掴めそうです。

実験編

調査で得られた知識を使って、普段書いているwith文を使ったファイル入出力を、コンテキストマネージャを使って書き換えてみました。

with open("sample.txt") as f:
  print(f.read())

ctxm = open("sample.txt") # コンテキストマネージャ(ファイルオブジェクト)を返す
f = ctxm.__enter__() # 自分自身を返す
try:
  print(f.read())
finally:
  f.__exit__() # 中でf.close()が呼び出される
print(f.closed)

実際にコードを実行してみると、f.closedTrueになり、ファイルをちゃんと閉じていたので、この書き換えはあながち間違いではないでしょう。

コンテキストマネージャを作る

今度は自分でコンテキストマネージャを作ってみます。

class Aisatsu:     
    def __enter__(self):    
        print("オハヨウ")
	return "こんばんは"
    def __exit__(self,*args):     
        print("オヤスミ")    
                 
with Aisatsu() as f:
    print("コニチハ")
    print(f)
❯ python3 test.py
オハヨウ
コニチハ
こんばんは
オヤスミ

print(f)ではこんばんはと表示されることに注意してください。
__enter__()で返された値が、as VARVARに入ります。

また、Pythonにはコンテキストマネージャを作るためのデコレータが用意されています。
こちらを使用して作った方が良さそうです。

from contextlib import contextmanager     
@contextmanager          
def aisatsu():           
    print("オハヨウ")        
    yield "こんばんは"    
    print("オヤスミ")   
with Aisatsu() as f:
    print("コニチハ")
    print(f)
❯ python3 test.py
オハヨウ
コニチハ
こんばんは
オヤスミ

解明編

調査と実験の結果から分かったことをまとめです。

  • with文は最初に何かして最後に必ず何かする(try/finally文)プログラムを一纏めにしたもの。
  • 「最初と最後に何かをする」は、コンテキストマネージャと呼ばれる__enter__()__exit__()の二つのメソッドを持つ型で表される。
  • with文のwith EXPREXPRは、コンテキストマネージャを指している。
  • ファイル操作でwith文が使えるのは、ファイルオブジェクトがコンテキストマネージャだから。

例えば、Stopwatchというオブジェクトは、最初にstart関数を実行して、最後にstop関数を実行したいとする。
コードで書くとすると、

a = Stopwatch()
a.start()
try:
  sleep(5)
finally:
  a.stop()

このコードのa.start()a.end()の部分を抽象化したものがコンテキストマネージャ型。
コンテキストマネージャ型は、__enter__()__exit__()の二つのメソッドを持つ。

a = ContextManager()
a.__enter__()
try:
  something()
finally:
  a.__exit__()

with文は、更にこのコードのコンテキストマネージャとtry/finally文を一纏めにしたもの。

with ContextManager() as x:
  something()

このコードのas xxには、__enter__()で返された値が入る。

というわけで、with文はファイル入出力に限らず、コンテキストマネージャと呼ばれる__enter__()__exit__()を持つものが相手ならば誰に対しても使うことができる。
また、コンテキストマネージャは誰でも簡単に作ることができる。

感想

本当は、面白そうなPythonのライブラリを見つけたので、その記事を書こうと思っていたのですが、ライブラリのコードの中でwith文を見つけてしまったがために、ついつい気になって調ベてしまったのが運の尽き。理解をするのに想定以上に時間がかかってしまい驚きました。
自分は、Pythonについて今までしっかりと勉強したことがなく、普段からフワッとした理解で使っていたので、勉強する良いきっかけになりました。
どの言語もそうだと思いますが、ドキュメントを潜り始めると、どんどん深みにハマっていく気がします。
ここまでお読みいただきありがとうございました。

Discussion