Pythonのwith文の正体
結論だけ知りたい人は解明編へ。
筆者はPythonistaではないため誤った理解をしている可能性があります。間違いなど見つけましたらご指摘いただけると嬉しいです。
with文との邂逅
Pythonを学び、ファイルの操作方法が分かった頃、初めてwith文というものを知りました。
それ以来、私はいつも当たり前のようにwith
文とopen()
関数をセットで使っていましたが、とあるコードを読んだ時に、ファイル操作以外に使われているのを発見しました。
そもそもwith文とは何なのか、その謎を解明すべく、我々はアマゾンの奥地へと向かった…
調査編
まずは、公式ドキュメントを読みます。
with 文は、ブロックの実行を、コンテキストマネージャによって定義されたメソッドでラップするために使われます (with文とコンテキストマネージャ セクションを参照してください)。
私のエンジニア力不足なのか、書いてあることがよく分かりません。
そもそもコンテキストマネージャとは何なのでしょうか。
「with文とコンテキストマネージャ セクションを参照しろ」とあるので参照します。
コンテキストマネージャとは
コンテキストマネージャ(context manager) とは、 with 文の実行時にランタイムコンテキストを定義するオブジェクトです。コンテキストマネージャは、コードブロックを実行するために必要な入り口および出口の処理を扱います。コンテキストマネージャは通常、 with 文( with 文 の章を参照)により起動されますが、これらのメソッドを直接呼び出すことで起動することもできます。
コンテキストマネージャの代表的な使い方としては、様々なグローバル情報の保存および更新、リソースのロックとアンロック、ファイルのオープンとクローズなどが挙げられます。
コンテキストマネージャについてのさらなる情報については、 コンテキストマネージャ型 を参照してください。
with文とコンテキストマネージャは密接に関わり合っているようですが、イマイチ良くわかりません。ただ、最後の1文にコンテキストマネージャ型とあるので、どうやらコンテキストマネージャはPythonの型の1つのようです。
ドキュメントの組み込み型一覧の中にもありました。
さらに、ユーザーがコンテキストマネージャ型を定義するために必要なメソッドを発見しました。
contextmanager.__enter__()
contextmanager.__exit__()
contextmanager.__enter__()
の説明にはこのようにあります。
自分自身を返すコンテキストマネージャの例として ファイルオブジェクト があります。ファイルオブジェクトは __enter__() から自分自身を返し、 open() が with 文のコンテキスト式として使われるようにします。
ファイルオブジェクトはコンテキストマネージャ...?
正直、自分の理解はまだフワッとしていますが、何となく概要は掴めてきました。
要するにコンテキストマネージャとは、__enter()__
と__exit()__
を持つPythonの型という事でいいでしょう。
あまり奥に進むと沼にハマってしまいそうなので、次に進みます。
with文とは
それでは、本記事のメインであるwith
文の方を掘り進めてみます。
PEP 343に、with
文の仕様、背景、および例が記載されていました。
最初の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.closed
がTrue
になり、ファイルをちゃんと閉じていたので、この書き換えはあながち間違いではないでしょう。
コンテキストマネージャを作る
今度は自分でコンテキストマネージャを作ってみます。
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 VAR
のVAR
に入ります。
また、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 EXPR
のEXPR
は、コンテキストマネージャを指している。 - ファイル操作で
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 x
のx
には、__enter__()
で返された値が入る。
というわけで、with文はファイル入出力に限らず、コンテキストマネージャと呼ばれる__enter__()
と__exit__()
を持つものが相手ならば誰に対しても使うことができる。
また、コンテキストマネージャは誰でも簡単に作ることができる。
感想
本当は、面白そうなPythonのライブラリを見つけたので、その記事を書こうと思っていたのですが、ライブラリのコードの中でwith
文を見つけてしまったがために、ついつい気になって調ベてしまったのが運の尽き。理解をするのに想定以上に時間がかかってしまい驚きました。
自分は、Pythonについて今までしっかりと勉強したことがなく、普段からフワッとした理解で使っていたので、勉強する良いきっかけになりました。
どの言語もそうだと思いますが、ドキュメントを潜り始めると、どんどん深みにハマっていく気がします。
ここまでお読みいただきありがとうございました。
Discussion