技術雑食系車好きの備忘録

今やってるもの:python・サーバ構築・blender

<Python>with構文の作動原理を理解する

はじめに

Javaで開発をしていた際、jarファイルなどのライブラリを眺めていると「元はどんなソースで書かれたものなんだろう」と気になり、逆コンパイルして追跡したくなる趣向があります。
Pythonでファイル操作に使用されるwith構文を学んだ際、その深淵が気になったので探ってみます。

結論だけ知りたい方は、最後のまとめ部分まで飛ばしてください。

※注意
個人的な解釈がふんだんに使われている内容なので、実際の言語仕様とは異なる可能性があります。
あくまで個人的な納得優先です、、、

with構文とは

Pythonにおけるwith構文とは、簡単に言えばファイルの読み書きにおいて前後処理を担ってくれる組み込み関数みたいなものです。
一般的に、ファイル読み書きにおいては「ファイルを開く」「ファイルを閉じる」処理はそれぞれ前後処理として明示的にプログラミングする事が安全と言われています。

※例

with open("sample.txt", "w") as fw:
    fw.write("test")

例えば上記のプログラムをwith構文を使用せずに書くと

fw = open("sample.txt", "w")
fw.write("test")
fw.close()

となります。

with構文無しだと前後処理を別々にプログラムする必要がありますが、with構文を使えばwithの1行が前後処理を担ってくれるのです。
ちゃんとした言い方をするなら、「特定の処理の前処理と後処理を設定することで、その処理をより簡潔かつ安全に利用できるようにするもの」ということ。

with構文の作動原理

では、with構文はどこにプログラムの本体が存在するのか探っていきます。
探っていく上での観点は2つ。
1.プログラム上の「with」が、with構文であることを判断するプログラムはどのように書かれているのか
2.前後処理はどのようなプログラムで書かれているのか

with構文とは何者か

まず、プログラム上にwithが出現した際に、「with構文が出たぞォォォ」をどう判断しているのか探ります。
そもそもwith構文は、公式曰く「コンテキストマネージャによって定義されたメソッド」です。
簡単に言えば、「コンテキストマネージャ」の一部品で、「withが出たぞォ(ry」はコンテキストマネージャで判断していると言えます。
>公式ドキュメント
docs.python.org

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

じゃあ「コンテキストマネージャ」とは何かと言うと、インタプリタに組み込まれている関数の一種です。
docs.python.org

平たく言えば、「Pythonの実行環境に元々設定されている部品」という感じでしょうか。
そのため、「withが出(ry」の判断は、Pythonが使用するPCのメモリ上に展開されている = 人の目には見えない場所に定義されているものだと解釈します。(どこを探してもそれらしいファイルが無いので)

よって、with構文であることを判断する処理は、文法が間違っていなければPython側がよしなに実行してくれるものと理解することにしました。


with構文における処理の中身

with構文を判断する処理は目に見えませんが、前後処理が書かれているプログラムはPythonの言語リファレンスとライブラリの内容を照らして、推測ながらそれらしい部分を発見できました。
>リファレンス
docs.python.org

>ライブラリ:contextlib.py(一部抜粋)

class nullcontext(AbstractContextManager):
    """Context manager that does no additional processing.

    Used as a stand-in for a normal context manager, when a particular
    block of code is only sometimes used with a normal context manager:

    cm = optional_cm if condition else nullcontext()
    with cm:
        # Perform operation, using optional_cm if condition is True
    """

    def __init__(self, enter_result=None):
        self.enter_result = enter_result

    def __enter__(self):
        return self.enter_result

    def __exit__(self, *excinfo):
        pass

文章にすると長くなるので、フロー図にするとこんな感じです。

気付いた点としては以下の2つ。
特にファイルクローズをfinallyで実行していない事が意外でした。
・try/finallyを使用しているのではなく、ファイルオープン ~ ファイルクローズまで一直線の処理で走らせていること
・本処理でのエラーは、ファイルクローズ処理側にFalseで渡してその後の動きを委ねている(基本はエラー吐いて終わり)

色々実験してみようとも思いましたが、思いのほか深淵が深淵すぎてここで打ち切ろうと思います。。。
気が向いたらまた何かやってみようかと。


まとめ

with構文は調べると色々理解が深まるが、闇が激深なので
ファイルオープンとクローズを担ってくれる構文と覚えておけばヨシ

参考:
https://djangobrothers.com/blogs/with_statement_basic/
https://zenn.dev/k41531/articles/9c566a778b79ca