1. ホーム
  2. python

[解決済み] Pythonの構文に新しいステートメントを追加することはできますか?

2022-07-14 02:53:26

質問

新しいステートメントを追加することはできますか? print , raise , with ) をPythonの構文に置き換えるか?

例えば、許可するために...

mystatement "Something"

または

new_if True:
    print "example"

もしあなたが べきである というより、それが可能かどうか(python インタープリタのコードを変更することなく)。

どのように解決するのですか?

次のページが参考になります。 Python内部:Pythonに新しいステートメントを追加する , こちらを引用しています。


この記事は、Pythonのフロントエンドがどのように動作するかをよりよく理解するための試みです。ドキュメントやソースコードを読むだけでは少し退屈かもしれませんので、ここでは実践的なアプローチを取っています。この記事では until をPythonに追加してみます。

この記事のすべてのコーディングは、最先端のPy3kブランチに対して行われました。 Python Mercurial リポジトリのミラー .

until ステートメント

Rubyのように、いくつかの言語では until ステートメントがあり、これは while ( until num == 0while num != 0 ). Rubyでは、書ける。

num = 3
until num == 0 do
  puts num
  num -= 1
end

と印刷されます。

3
2
1

そこで、Pythonにも同じような機能を追加したいと思います。つまり、書けるようになることです。

num = 3
until num == 0:
  print(num)
  num -= 1

言語擁護のための余談

この記事は、言語擁護のために until 文の追加を推奨するものではありません。そのような文はいくつかのコードをより明確にすると思いますし、この記事はそれがいかに簡単に追加できるかを示していますが、私はPythonの最小主義の哲学を完全に尊重しています。私がここでやろうとしていることは、本当に、Pythonの内部動作についていくらかの洞察を得ることです。

文法を変更する

Python はカスタムパーサージェネレータである pgen . これは Python のソースコードを解析木に変換する LL(1) パーサーです。パーサジェネレータへの入力は、ファイル Grammar/Grammar [1] . これは、Pythonの文法を指定する簡単なテキストファイルです。

[1] : これ以降、Pythonソース中のファイルへの参照は、ソースツリーのルート(Pythonをビルドするためにconfigureやmakeを実行したディレクトリ)を相対的に指定することになります。

文法ファイルには、2つの修正が必要です。1つ目は until 文の定義を追加することです。私は、どこに while ステートメントが定義されていた場所 ( while_stmt ) を追加し、さらに until_stmt の下に [2] :

compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite

[2] : これは、私がよく知らないソースコードを修正するときに使用する一般的なテクニックを示しています。 類似性による作業 . この原則はすべての問題を解決するわけではありませんが、間違いなくプロセスを容易にすることができます。のために行わなければならないことは、すべて while に対しても行わなければならないので until も行わなければならないが、これはかなり良いガイドラインになる。

を除外することにしたことに注意してください。 else の定義から until という節がありますが、これは少し違います。 else 節が嫌いで、PythonのZenにうまく適合しないと思うからです)。

2つ目の変更点は compound_stmt を含むように変更することです。 until_stmt を含めるようにしました。これは while_stmt の直後です。

を実行すると make を変更した後 Grammar/Grammar を変更すると pgen を再 生成するために実行されます。 Include/graminit.hPython/graminit.c で、いくつかのファイルが再コンパイルされます。

AST生成コードの修正

Pythonパーサーが解析木を作成した後、この木はASTに変換されます。ASTは を扱うのがより簡単だからです。 であるためです。

そこで Parser/Python.asdl にアクセスし、ASTノードを追加して、新しい until 文のすぐ下に、やはり while :

| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)

もし今、あなたが make を実行すると、ファイルの束をコンパイルする前に Parser/asdl_c.py が実行され、AST定義ファイルからCコードを生成していることに注意してください。これは(例えば Grammar/Grammar のように)Pythonのソースコードがプログラミングを簡単にするためにミニ言語(言い換えればDSL)を使用するもう一つの例です。また Parser/asdl_c.py はPythonスクリプトであり、これは一種の ブートストラップ - であり、Pythonをゼロからビルドするために、Pythonはすでに利用可能でなければなりません。

一方 Parser/asdl_c.py は新しく定義された AST ノードを管理するためのコードを生成しました (ファイル Include/Python-ast.hPython/Python-ast.c など)であっても、関連するパースツリーのノードをそれに変換するコードを手作業で書かなければなりません。これは、ファイル Python/ast.c . という名前の関数があります。 ast_for_stmt という関数がステートメントのパースツリーノードをASTノードに変換しています。ここでも、古い友人である while に導かれ、私たちはすぐに大きな switch に飛び込んで、複合文の処理のために until_stmt :

case while_stmt:
    return ast_for_while_stmt(c, ch);
case until_stmt:
    return ast_for_until_stmt(c, ch);

次に ast_for_until_stmt . ここにあります。

static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
    /* until_stmt: 'until' test ':' suite */
    REQ(n, until_stmt);

    if (NCH(n) == 4) {
        expr_ty expression;
        asdl_seq *suite_seq;

        expression = ast_for_expr(c, CHILD(n, 1));
        if (!expression)
            return NULL;
        suite_seq = ast_for_suite(c, CHILD(n, 3));
        if (!suite_seq)
            return NULL;
        return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
    }

    PyErr_Format(PyExc_SystemError,
                 "wrong number of tokens for 'until' statement: %d",
                 NCH(n));
    return NULL;
}

これもまた、相当する ast_for_while_stmt をよく見ながらコーディングされています。 until をサポートしないことにしました。 else 節をサポートしないことにしました。予想通り、ASTは再帰的に作成され、他のAST作成関数である ast_for_expr といった他のAST作成関数を用いて再帰的に作成されます。 ast_for_suite の本文は until ステートメントの本文に使用します。最後に、新しいノードである Until という名前の新しいノードが返されます。

解析木ノードにアクセスすることに注意してください。 n のようないくつかのマクロを使って NCHCHILD . これらは理解する価値があります - それらのコードは Include/node.h .

余談ですが ASTの構成

のために新しいタイプのASTを作成することにしました。 until 文のために新しいタイプの AST を作成することにしましたが、実はこれは必要ではありません。私はいくつかの作業を節約し、既存の AST ノードの合成を使用して新しい機能を実装することができました。

until condition:
   # do stuff

と機能的に同等である。

while not condition:
  # do stuff

を作成する代わりに Until ノードを ast_for_until_stmt を作成することができました。 Not ノードに While ノードを子として持つ。ASTコンパイラはこれらのノードをどのように扱うか既に知っているので、次のステップはスキップすることができます。

ASTをバイトコードにコンパイルする

次のステップは、ASTをPythonのバイトコードにコンパイルすることです。コンパイルにはCFG(Control Flow Graph)という中間結果がありますが、同じコードで処理するため、この詳細は今は無視し、別の記事で紹介します。

次に見ていくコードは Python/compile.c . のリードに続いて while を導くと、関数 compiler_visit_stmt この関数はステートメントをバイトコードにコンパイルする役割を担っています。に対する節を追加します。 Until :

case While_kind:
    return compiler_while(c, s);
case Until_kind:
    return compiler_until(c, s);

もし、あなたが Until_kind は何かというと、これは定数です(実際には _stmt_kind の列挙の値) で、AST 定義ファイルから自動的に Include/Python-ast.h . とにかく、私たちは compiler_until を呼び出します。もちろん、これはまだ存在しません。これはもちろんまだ存在しません。

もしあなたが私のように好奇心が旺盛なら、次のことに気づくでしょう。 compiler_visit_stmt が特殊であることに気づくでしょう。いくら grep -をいくら探しても、それが呼び出される場所はわかりません。この場合、残る選択肢はただ一つ、Cのマクロフーです。実際、少し調べると VISIT で定義されたマクロに行き着きます。 Python/compile.c :

#define VISIT(C, TYPE, V) {\
    if (!compiler_visit_ ## TYPE((C), (V))) \
        return 0; \

を呼び出すために使われます。 compiler_visit_stmtcompiler_body . しかし、我々の仕事に戻ると....

約束通り、ここで compiler_until :

static int
compiler_until(struct compiler *c, stmt_ty s)
{
    basicblock *loop, *end, *anchor = NULL;
    int constant = expr_constant(s->v.Until.test);

    if (constant == 1) {
        return 1;
    }
    loop = compiler_new_block(c);
    end = compiler_new_block(c);
    if (constant == -1) {
        anchor = compiler_new_block(c);
        if (anchor == NULL)
            return 0;
    }
    if (loop == NULL || end == NULL)
        return 0;

    ADDOP_JREL(c, SETUP_LOOP, end);
    compiler_use_next_block(c, loop);
    if (!compiler_push_fblock(c, LOOP, loop))
        return 0;
    if (constant == -1) {
        VISIT(c, expr, s->v.Until.test);
        ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
    }
    VISIT_SEQ(c, stmt, s->v.Until.body);
    ADDOP_JABS(c, JUMP_ABSOLUTE, loop);

    if (constant == -1) {
        compiler_use_next_block(c, anchor);
        ADDOP(c, POP_BLOCK);
    }
    compiler_pop_fblock(c, LOOP, loop);
    compiler_use_next_block(c, end);

    return 1;
}

白状すると、このコードはPythonバイトコードの深い理解に基づいて書かれたものではありません。記事の他の部分と同じように、それはキンの模倣で行われました。 compiler_while 関数を真似たものです。しかし、注意深く読むことで、PythonのVMはスタックベースであることを念頭に置き、また、関数のドキュメントをちらっと見ることで dis モジュールのドキュメントをちらっと見るだけで、そのモジュールには Python バイトコードのリスト と記述することで、何が起こっているのかを理解することができます。

これで終わり...?そうでしょう?

すべての変更と make を実行した後、新しくコンパイルされた Python を実行し、新しい until ステートメントを試すことができます。

>>> until num == 0:
...   print(num)
...   num -= 1
...
3
2
1

ほら、うまくいきましたね。新しいステートメントに対応するバイトコードを dis モジュールを使って新しいステートメントのために作られたバイトコードを次のように見てみましょう。

import dis

def myfoo(num):
    until num == 0:
        print(num)
        num -= 1

dis.dis(myfoo)

その結果がこちらです。

4           0 SETUP_LOOP              36 (to 39)
      >>    3 LOAD_FAST                0 (num)
            6 LOAD_CONST               1 (0)
            9 COMPARE_OP               2 (==)
           12 POP_JUMP_IF_TRUE        38

5          15 LOAD_NAME                0 (print)
           18 LOAD_FAST                0 (num)
           21 CALL_FUNCTION            1
           24 POP_TOP

6          25 LOAD_FAST                0 (num)
           28 LOAD_CONST               2 (1)
           31 INPLACE_SUBTRACT
           32 STORE_FAST               0 (num)
           35 JUMP_ABSOLUTE            3
      >>   38 POP_BLOCK
      >>   39 LOAD_CONST               0 (None)
           42 RETURN_VALUE

最も興味深いのは12番の処理です。条件が真であれば、ループの後にジャンプします。これは、正しいセマンティクスで until . もしジャンプが実行されなければ、ループ本体は操作35で条件にジャンプして戻るまで走り続けます。

いい感じに変更できたので、次にこの関数を実行してみました(実行中の myfoo(3) を実行)してみました。その結果は、あまり心強いものではありませんでした。

Traceback (most recent call last):
  File "zy.py", line 9, in
    myfoo(3)
  File "zy.py", line 5, in myfoo
    print(num)
SystemError: no locals when loading 'print'

おっと...これはマズいぞ。で、何が悪かったんだ?

シンボルテーブルがない件

PythonコンパイラがASTをコンパイルする際に行うステップの1つは、コンパイルするコードのシンボルテーブルを作成することです。の呼び出しは PySymtable_BuildPyAST_Compile は、シンボル・テーブル・モジュール ( Python/symtable.c ) を呼び出します。このモジュールはコード生成関数と同じような方法で AST を走査します。各スコープのシンボルテーブルを持つことで、コンパイラはどの変数がグローバルでどの変数がスコープにローカルであるかといった重要な情報を把握することができます。

この問題を解決するために、私たちは symtable_visit_stmt の関数を修正する必要があります。 Python/symtable.c を処理するコードを追加し、さらに until ステートメントを処理するコードを追加しています。 while ステートメント [3] :

case While_kind:
    VISIT(st, expr, s->v.While.test);
    VISIT_SEQ(st, stmt, s->v.While.body);
    if (s->v.While.orelse)
        VISIT_SEQ(st, stmt, s->v.While.orelse);
    break;
case Until_kind:
    VISIT(st, expr, s->v.Until.test);
    VISIT_SEQ(st, stmt, s->v.Until.body);
    break;

[3] : ちなみに、このコードがないと、コンパイラの警告で Python/symtable.c . コンパイラは Until_kind の switch ステートメントで扱われていないことに気づきます。 symtable_visit_stmt の switch 文で処理されず、文句を言われます。コンパイラの警告を確認することは常に重要です!

そして、これで本当に完了です。この変更後のソースをコンパイルすると myfoo(3) が期待通りに動作するようになります。

結論

この記事では、Pythonに新しいステートメントを追加する方法を示しました。Pythonコンパイラのコードにかなり手を加える必要があるものの、類似した既存のステートメントをガイドラインとして使用したため、この変更を実装するのは難しくありませんでした。

Python コンパイラーはソフトウェアの洗練された塊であり、私はその専門家であると主張するつもりはありません。しかし、私は Python の内部、特にそのフロントエンドに本当に興味があります。そのため、この演習はコンパイラの原理やソースコードの理論的な勉強に非常に役立つと思います。これは、コンパイラーをより深く理解するための将来の記事のベースとして役立つでしょう。

リファレンス

この記事を書くにあたり、いくつかの優れた参考文献を使用しました。以下、順不同で紹介します。

  • PEP 339: CPython コンパイラの設計 - の最も重要かつ包括的な部分です。 公式 の公式ドキュメントの中で最も重要で包括的なものです。非常に短いので、Python の内部に関する良い文書が少ないことを痛感させられます。
  • Python Compiler Internals" - Thomas Lee による記事です。
  • "Python。Design and Implementation" - Guido van Rossum によるプレゼンテーション。
  • Python (2.5) Virtual Machine, A guided tour - Peter Tröger による発表。

オリジナルソース