Pythonのプリプロセッサ
(pydong.org)- Pythonでは、先頭2行の ソースエンコーディングのマジックコメント とユーザー定義 codec を使って、ファイル内容を実行前に書き換えたり、まったく別のコードに置き換えたりできる
- ユーザー定義 codec は、
.pthパス設定ファイル のimport実行を通じてインタプリタ初期化中に登録でき、codecs.registerで検索関数を追加する - codec の実装には
decode(data: bytes) -> tuple[str, int]と 増分デコーダ が必要で、例外処理をしないと原因の代わりにSyntaxError: encoding problem: your_codecだけが表示されることがある ++/--のインクリメント・デクリメント演算子、波かっこベースの Python、cppyyによる C/C++ 実行、TOML を JSON Schema で検証する方式まで、同じエントリポイントで実装できる- お遊びの例を超えて、
pythonql、future-typing、future-fstrings、future-annotationsのような Python 拡張・バックポートにも使え、magic_codecは反復作業を減らしてくれる
ソースエンコーディングをプリプロセスの入口として使う
- PEP-0263 に従い、Python ファイルの先頭2行のいずれかで ソースコードのエンコーディング を指定できる
- 例:
# coding=utf8,# -*- coding: utf8 -*-,# vim: set fileencoding=utf8 :
- 例:
- マジック行は正規表現
^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)に一致する必要がある- codec 名は
[-_.a-zA-Z0-9]+に一致していなければならない
- codec 名は
- ユーザー定義 codec は、ソースをデコードするだけでなく、ソース文字列を書き換えたうえで Python インタプリタに渡せる
.pth ファイルで codec を登録する
- Python インタプリタが
-Sオプションなしで起動すると、siteパッケージが初期化中に読み込まれる site-packages内の.pthパス設定ファイル は、空行と#で始まる行を除いた内容をモジュール検索パスに追加する- Python のドキュメントによれば、
importの後に空白またはタブが続く行は実行される- 例:
packagename.pthにimport packagename.register_codecを入れると、初期化中にそのモジュールが import される
- 例:
- import されたモジュールは
codecs.registerを呼び出して codec 検索関数を登録できる- import は一度しか実行されないため、検索関数の登録も一度だけでよい
ユーザー定義 codec の実装
- ユーザー定義 codec には2つが必要
decode(data: bytes) -> tuple[str, int]- 増分デコーダ クラス
decode関数はcodecs.utf_8_decodeで実際の UTF-8 デコードを行い、結果の文字列をプリプロセス関数に渡せる- codec 内部の例外が捕捉されないと、通常の traceback の代わりに
SyntaxError: encoding problem: your_codecだけが出力されることがある- プリプロセス関数で発生した例外は、
traceback.print_exc()で直接表示してから再度 raise するほうがよい
- プリプロセス関数で発生した例外は、
- 増分デコーダはファイル全体をバッファにため、最後の
decode呼び出しで一度だけプリプロセスできる- 実装例では
codecs.BufferedIncrementalDecoderを継承し、decode(self, data, final=False)でfinalのときだけ処理する
- 実装例では
- プリプロセス結果は元のファイル内容を必ずしも使う必要はなく、完全に任意の Python コードを返してもよい
- ただし先頭行はマジック行として期待されるため削除され、結果は有効な Python でなければならない
Python 構文拡張の例
-
++と--のインクリメント・デクリメント演算子- Python には単項の インクリメント・デクリメント演算子 がない
x++,x--は構文的に無効++x,--xは構文上は有効だが、それぞれx.__pos__().__pos__(),x.__neg__().__neg__()の呼び出しになる- プリプロセッサはトークンストリームを書き換え、インクリメント・デクリメント演算子のように動作させられる
x++→(x, x := x + 1)[0]x--→(x, x := x - 1)[0]++x→(x, x := x + 1)[1]--x→(x, x := x - 1)[1]
- この変換は Python の代入式である ウォルラス演算子 (walrus operator) を使う
- 単純なトークン置換だけでは
x++ - -yのような式で失敗することがあり、x++ - (-y)のようにかっこで曖昧さを減らせる - incdec.py は正規表現で置換するが、文字列リテラル内部の置換を避けようとしても脆弱になりうる
- トークンストリームを直接変更する実装は magic.incdec にある
-
波かっこベースの Python
from __future__ import bracesはSyntaxError: not a chanceを発生させる- プリプロセッサはトークンストリームを修正し、波かっこスコープ をインデントベースの Python に変換できる
- 実装の流れは次の通り
tokenize.generate_tokensでトークンを生成する- 入力文字列は
io.StringIOのreadlineで1行ずつ渡す - 既存の
INDENT,DEDENTトークンは削除する {に出会ったらインデントレベルを上げて:を出力する}に出会ったらインデントレベルを下げるNLの後には現在のインデントレベルに応じたINDENTトークンを追加する
- Python の辞書リテラルとの衝突を減らすには、
{の後ろが newline のときだけインデントレベルを調整し、}の前が newline のときだけ閉じるスコープとして扱える - 複数行の辞書はバックスラッシュを使えば、波かっこの内部に newline トークンが発生しないため利用できる
他言語を Python として実行する
-
C と C++
#でコメントを書くシェルスクリプト、CMake スクリプト、PHP、Ruby のような言語は、shebang と一緒にエンコーディングのマジック行を入れやすい- C と C++ はコメントが
/* ... */または// ...だが、プリプロセッサディレクティブは#で始まるため、エンコーディング用の正規表現に一致させられる - 例のマジック行は C/C++ のソースでも有効であり、Python のエンコーディングパターンにも一致する
#define CODEC "coding:magic.cpp"
- cppyy を使うと、Python から C/C++ コードを解釈し、バインディングを生成できる
cppyyは内部的にclingを使う
- プリプロセス結果はおおむね次のような Python コードになる
import cppyycppyy.cppdef("<input source file content>")from cppyy.gbl import main__name__ == "__main__"のときに C/C++ のmain()を呼び出す
- 実装例は magic.cpp にある
TOML 検証ツールとして使う
- TOML は
#でコメントを始めるため、# coding: magic.tomlのようなエンコーディングのマジック行を入れられる - プリプロセス結果を Python の検証スクリプトに変換すれば、Python インタプリタを TOML 検証ツール のように使える
- 検証例では次のモジュールを使用する
tomllibで TOML ファイルを読むjsonで JSON Schema ファイルを読む- jsonschema で検証する
- 実行例:
python tests/toml/data_valid.toml -s tests/toml/schema.json- 有効なら
Successfully validated.を出力する
- 不正な TOML の例では、
scores配列内の文字列'20'が数値ではないという検証エラーが出力される - 実装例は magic.toml にある
実運用と magic_codec
- ユーザー定義 codec と
.pthファイルを組み合わせると、Python インタプリタの動作を大きく変えられる - 例の大半は遊び向けだが、実際の活用例もある
- pythonql: Python 向けクエリ言語拡張
- future-typing: Python 3.6+ にジェネリック型ヒントと
|union 構文をバックポート - future-fstrings
- future-annotations
site-packagesを直接いじったり、.pthファイルや反復コードを自分で書いたりしたくないなら、magic_codec を使えるmagic_codec拡張はmagic_接頭辞の付いた Python パッケージとして作成できる- ファイル codec を
magic_fooに設定すると、magic_fooパッケージを読み込む - そのパッケージに
preprocess関数があるか確認する
- ファイル codec を
- 想定される
preprocessのシグネチャは次の通りdef preprocess(data: str) -> str:
- 拡張例は example/ にある
1件のコメント
Hacker News のコメント
from __future__ import bracesを実行するとSyntaxError: not a chanceが出るこの面白いエラーメッセージは、2001年から CPython にハードコードされていたhttps://github.com/python/cpython/commit/ad3d3f2f3f19833f59f...
作者の Jeremy Hylton は現在 Google で AI 検索品質を担当する Principal Engineer として働いているが、24年の間に、禁止された構文を遊び心で記念していたところから、専用の構文を必要としない汎用的なクエリシステムへとキャリアがつながっているのはなかなか印象的
break rust;が Rust コンパイラで内部コンパイラエラーを出していた件を思い出す。他の言語にも似たようなイースターエッグがどれだけあるのか気になる遊び心のある非公式な趣味のハックが、実際のプロの開発とは別世界だというのは誤解だと思う
https://news.ycombinator.com/item?id=41314393
import hook でふざけるのがクビになる最も創造的な方法だと思っていたが、それは甘い考えだった。codec の正規表現のせいで
μtf8みたいなものでうまくトロールできなさそうなのは残念で、これからは import hook、プリプロセッサ、sys.settraceを使って全関数を直前に呼ばれた関数へモンキーパッチし、17分ごとに stdout と stderr を入れ替えるしかなさそうPython がプリプロセッサフックを意図的に表に出していないのには十分な理由があり、分別ある大人なら近づくべきではないと思う部分がある
しかし一方で、分別ある大人たちとは関わりたくない。実に面白いことがたくさんできそうだ
public/privateの可視性指定子がなく、おそらくあらゆるメタプログラミングの魔法も開放しているのだと思う。「分別ある大人」を心配していたなら、こうした設計判断は少し奇妙だったはず :)これは便利で本当に役立ちそう。馬鹿げた import ハックをするときは、たいていモジュールを import して
astモジュールでコードを書き換え、execした後にexit()を挟み込んでいたが、プリプロセッサがあればずっと使いやすそうすべての
dictが順序を保証するようになる前は、astの書き換えでリストリテラルを ordered dict 呼び出しに変えるのに主に使っていて、実際に役立っていたPython の柔軟さが好きだ。自分がやった中で最も呪われたことは文字列をその場で変更したことで、最終的には
mmapまで乱用して自己変更するスクリプトを書く羽目になった。今度は producer として Lisp インタプリタを書くべきな気がしているctypesみたいなものでメモリ位置に直接書き込んだのか?これまで見つけた最高の用途は JSX に着想を得た pyxl: https://github.com/dropbox/pyxl
こんなコードが書ける
# coding: pyxlprint Hello World!これが Python 2 から 3 への移行をよりうまく扱うのに使えたのか気になる。例えば
# coding: six.python2が Python 2 コードを有効な Python 3 コードに合わせたり、# coding: six.python3が Python 3 コードを Python 2 で動くように変えたりするような形b"..."やu"..."のプレフィックスを追加・削除することもできたかもしれないdictのキーに使うと同じ項目を指していたが、Python 3 では同じ ASCII 内容のbytesとstrが同じdict内で別々の項目を指すさらに厄介な変更もある。
.keys()や.values()のような複数の組み込み機能は、Python 2 ではリストを返すが、Python 3 ではイテレータを返す。sixのユーティリティや他の回避策でコードを安全に翻訳しようとすると、コードが非常に冗長になる。ほとんどは一度しか使われないが、時には二度使われることがあるためだimport 時点でコードを書き換えられるツールがあるなら、むしろ変換後のコードをコミットして段階的に整理するほうがよいと思う。難しい部分は
str対bytesのように、遠く離れたコードにまで影響する動作変化だこのcoding hook 戦略で生じた依存関係は
pip freezeやuvが検出してくれるのだろうか?そうでないなら楽しい時間になりそう :). 誰かがこういうものを仕込んでいたなら、他にどんな罠があるかもほぼ保証されたようなものなので、そうしたドラゴンと戦うくらいならライブラリを書き直すほうが簡単そう
擬似コードの Python を作って、それを LLM にデコードさせたらかなり笑えそう。もちろんひどいものになるだろうが、面白くはありそう