1 ポイント 投稿者 GN⁺ 2024-08-23 | 1件のコメント | WhatsAppで共有

Pythonのプリプロセッサ

  • Pythonにプリプロセッサがないという主張は事実ではない
  • Pythonには非常に強力なプリプロセッサがある

Pythonソースコードのエンコーディング

  • PEP-0263 によりソースコードのエンコーディングを定義できる
  • 先頭2行にマジックコメントを追加してエンコーディングを設定できる
  • 例: # coding=utf8, # -*- coding: utf8 -*-, # vim: set fileencoding=utf8 :

パス設定ファイル(.pth)

  • Pythonインタプリタが -S オプションなしで起動すると、site パッケージが自動的に読み込まれる
  • site-packages フォルダに .pth ファイルを追加してモジュール探索パスを拡張できる
  • .pth ファイル内で import で始まる行は実行される
  • これにより、Pythonインタプリタの初期化時に任意のコードを実行できる

カスタムコーデックの定義

  • Pythonインタプリタが満たすべき要件は2つある:
    • decode(data: bytes) -> tuple[str, int] 関数
    • インクリメンタルデコーダクラス
  • codecs.utf_8_decode を使って実際のデコードを行い、結果の文字列をプリプロセッサに渡す
  • 例外を捕捉して出力し、再送出するのが望ましい

インクリメンタルデコーダの提供

  • codecs.BufferedIncrementalDecoder を継承してインクリメンタルデコーダを実装する
  • バッファにデータを収集し、最終デコード呼び出し時にファイル全体を前処理する

Pythonの拡張

  • Pythonの標準ライブラリを使ってPythonを拡張するのは比較的容易
  • tokenize モジュールでファイルのトークンストリームを変更したり、ast モジュールで抽象構文木を変更したりできる
単項インクリメントとデクリメント
  • Pythonには単項インクリメント演算子とデクリメント演算子がない
  • x++, x-- は無効
  • ++x, --x は有効だが、別の意味を持つ
  • 単項インクリメント/デクリメント式をPythonの式に変換できる
  • 入力ファイル incdec.py:
    # coding: magic.incdec
    i = 6
    assert i-- == 6
    assert i == 5
    assert ++i == 6
    assert --i == 5
    assert i++ == 5
    assert i == 6
    assert (++i, 'i++') == (7, 'i++')
    print("PASSED")
    
  • 変換後のファイル:
    i = 6
    assert ((i, i := i - 1)[0]) == 6
    assert i == 5
    assert ((i, i := i + 1)[1]) == 6
    assert ((i, i := i - 1)[1]) == 5
    assert ((i, i := i + 1)[0]) == 5
    assert i == 6
    assert (((i, i := i + 1)[1]), 'i++') == (7, 'i++')
    print("PASSED")
    

波かっこを使うPython(Bython)

  • Pythonのインデントの代わりに波かっこでスコープを指定できる
  • tokenize.generate_tokens を使ってトークンストリームを変更する
  • 入力ファイル test.by:
    # coding: magic.braces
    def print_message(num_of_times) {
      for i in range(num_of_times) {
        print("braces ftw")
      }
      print({'x': 3})
    }
    x = {
      'foo': 42,
      'bar': 5
    }
    if __name__ == "__main__" {
      print_message(2)
      print({k: v for k, v in x.items()})
    }
    
  • 変換後のファイル:
    def print_message(num_of_times):
      for i in range(num_of_times):
        print("braces ftw")
      print({'x': 3})
    x = {
      'foo': 42,
      'bar': 5
    }
    if __name__ == "__main__":
      print_message(2)
      print({k: v for k, v in x.items()})
    

他言語の解釈

  • Pythonインタプリタに他の言語を解釈させることができる
  • 例: C, C++, TOML
  • C++ファイル test.cpp:
    #define CODEC "coding:magic.cpp"
    #include <cstdio>
    int main() {
      puts("Hello World");
    }
    
  • 変換後のファイル:
    import cppyy
    cppyy.cppdef(r"""
    #define CODEC "coding:magic.cpp"
    #include <cstdio>
    int main() {
      puts("Hello World");
    }
    """)
    from cppyy.gbl import main
    if __name__ == "__main__":
      main()
    

データ検証

  • TOML形式のデータをJSON Schemaで検証できる
  • jsonschema を使って実際の検証を行う
  • スキーマファイル schema.json:
    {
      "type": "object",
      "properties": {
        "name": {"type": "string"},
        "age": {"type": "number"},
        "scores": {
          "type": "array",
          "items": {"type": "number"}
        },
        "address": {"$ref": "#/$defs/address"}
      },
      "required": ["name"],
      "$defs": {
        "address": {
          "type": "object",
          "properties": {
            "street": {"type": "string"},
            "postcode": {"type": "number"}
          },
          "required": ["street"]
        }
      }
    }
    
  • 有効なデータファイル data_valid.toml:
    # coding: magic.toml
    name = "John Doe"
    age = 42
    scores = [40, 20, 80, 90]
    [address]
    street = "Grove St. 4"
    postcode = 19201
    
  • 無効なデータファイル data_invalid.toml:
    # coding: magic.toml
    name = "John Doe"
    age = 42
    scores = [40, "20", 80, 90]
    [address]
    street = "Grove St. 4"
    postcode = 19201
    

結論

  • カスタムコーデックとパス設定ファイルを使うことで、Pythonインタプリタの動作を大きく変更できる
  • 例としては pythonql, future-typing, future-fstrings, future-annotations などがある
  • magic_codec を使えば、自分自身のプリプロセッサを簡単に試せる

GN⁺の要約

  • Pythonのプリプロセッサを活用して、さまざまな言語拡張やデータ検証を行える
  • カスタムコーデックを通じてPythonインタプリタの動作を変更する方法を説明している
  • この記事はPython開発者にとって有用なツールと手法を提供する
  • 類似の機能を持つプロジェクトとして pythonql, future-typing などがある

1件のコメント

 
GN⁺ 2024-08-23
Hacker Newsのコメント
  • from __future__ import braces の構文エラーメッセージは、2001年から cpython にハードコードされている

    • Jeremy Hylton が書いたもので、現在は Google で AI 検索品質を担当するシニアエンジニアとして働いている
    • 24年間で、ある人物のキャリアが特定の構文禁止を記念することから始まり、専用構文を必要としない検索システムに取り組むまで発展したことに驚く
  • import-hooks を使った創造的な解雇方法を考えたが、codec regex が "μtf8" のようなものを使えないようにしていて残念

    • import hooks、preprocessors、sys.settrace を使って、すべての関数を直前に呼ばれた関数へモンキーパッチし、17分ごとに stdout と stderr を入れ替える方法を使う必要がある
  • python がプリプロセッサフックを公開していないのには理由があり、分別のある大人なら避けるべきだと思う

    • しかし、分別のある大人かどうかに関係なく、楽しさを追求したい
  • プリプロセッサのほうがより便利で有用だ

    • ast モジュールを使ってコードを書き換え、exec で実行してから exit() を挿入する形でハックしていた
    • dicts がすべて順序保持されるようになる前は、ast の書き換え機能を便利に使っていた
  • python の柔軟性が大好きだ

    • 最も呪われた作業は文字列をその場で変形することで、mmap を悪用してスクリプト自身を書き換えるようにした
    • 今は Lisp インタプリタを書きたくなっている
  • pyxl を使った最高の事例は jsx に触発されたもの

    • # coding: pyxl を使って HTML コードを書ける
  • Python 2 から 3 への移行をもっとうまく処理できたのではないかと思う

    • # coding: six.python2 は Python2 のコードを Python3 で有効にし、# coding: six.python3 は Python3 のコードを Python2 で実行可能なように調整できる
  • このアイデアを気に入ってくれてうれしい。近いうちにさらに多くの内容を出す予定

  • 久しぶりに、まったく新しいアイデアに驚かされた

  • Python でインラインコード生成をしたいなら、Ned Batchelder の cog を使える

  • この coding hook 戦略で導入された依存関係が pip freeze や uv によって検出されるのか気になる

    • そうでないなら、どうぞ楽しんで使ってほしい。ライブラリを書き換えるほうが簡単かもしれない(こういう落とし穴があるなら、ほかにも落とし穴がある可能性が高い)