ホームページ > バックエンド開発 > Python チュートリアル > Python バイトコードでネストされた関数のサポートを追加した方法

Python バイトコードでネストされた関数のサポートを追加した方法

Susan Sarandon
リリース: 2024-12-31 18:58:18
オリジナル
885 人が閲覧しました

How I added support for nested functions in Python bytecode

いくつかのかなりクールなものを共有したいと思いました。ネストされたサポートを追加した方法など、Python バイトコード について学習してきました。関数はありますが、印刷所の人は 500 ワード以内にする必要があると言っていました。

今週は休日ですと彼は肩をすくめました。 私に何を期待していますか?

コード スニペットを除く、私は交渉しました。

わかりました、彼は譲歩しました。

そもそもなぜバイトコードを使うか知っていますか?

私は印刷機を操作しているだけですが、あなたを信頼しています。

まあまあです。始めましょう。

そもそもなぜバイトコードを使うのか

Rust で書かれた私の Python インタプリタである Memphis には 2 つの実行エンジンがあります。どちらもすべてのコードを実行できるわけではありませんが、一部のコードは実行できます。

私の ツリーウォーク インタプリタ は、自分が何をしているのか分からない場合に構築するものです。 ?‍♂️ 入力 Python コードをトークン化し、抽象構文ツリー (AST) を生成し、ツリーをたどって 各ノードを評価します。式は値を返し、ステートメントはシンボル テーブルを変更します。シンボル テーブルは、Python スコープ ルールを尊重する一連のスコープとして実装されます。簡単な肺炎 LEGB を覚えておいてください: ローカル、エンクロージング、グローバル、ビルトイン。

私の バイトコード VM は、自分が何をしているのかは分からないが、知っているように行動したい場合に構築するものです。また?‍♂️。このエンジンでは、トークンと AST は同じように機能しますが、歩くのではなく全力疾走を始めます。 AST を、以降バイトコードとして知られる中間表現 (IR) にコンパイルします。次に、スタックベースの仮想マシン (VM) を作成します。これは概念的には CPU のように機能し、バイトコード命令を順番に実行しますが、完全にソフトウェアで実装されます。

(とりとめのない両方のアプローチの完全なガイドについては、Crafting Interpreters が優れています。)

そもそもなぜこれを行うのでしょうか?携帯性とパフォーマンスという 2 つの P を覚えておいてください。 2000 年代初頭、Java バイトコードの移植性について誰も黙らなかったことを覚えていますか? 必要なのは JVM だけで、どのマシンでもコンパイルされた Java プログラムを実行できます! Python は技術的およびマーケティング上の理由からこのアプローチを採用しませんでしたが、理論的には同じ原則が当てはまります。 (実際には、コンパイル手順が異なり、このワームの缶を開けたことを後悔しています。)

しかし、パフォーマンスが重要です。プログラムの存続期間中に AST を何度も走査するよりも、コンパイルされた IR の方が効率的な表現となります。 AST を繰り返し走査するオーバーヘッドを回避することでパフォーマンスが向上し、そのフラットな構造により、実行時の分岐予測とキャッシュの局所性が向上することがよくあります。

(コンピューター アーキテクチャのバックグラウンドがないのにキャッシュについて考えなかったということを責めるつもりはありません。私はその業界でキャリアをスタートしましたが、キャッシュについて考えることは、回避方法について考えるよりもはるかに少ないです。同じコード行を 2 回書くので、パフォーマンスに関しては私を信頼してください。それが私のリーダーシップ スタイルです。)

やあ、これは 500 単語です。フレームをロードしてリッピングする必要があります。

もう?!コード スニペットを除外しましたか?

コード スニペットはありません。

わかりました、わかりました。あと500個だけ。約束します。

Python 変数はコンテキストが重要

約 1 年前、バイトコードの VM 実装を表にまとめるまでにはかなりの時間がかかりました。Python の関数とクラスを定義し、それらの関数を呼び出し、それらのクラスをインスタンス化することができました。私はいくつかのテストでこの動作を取り締まりました。しかし、私の実装は雑で、さらに面白いものを追加する前に、基本を再検討する必要があることはわかっていました。クリスマス週間なので、楽しいことを追加したいと思います。

TODO に注目しながら、関数を呼び出すためのこのスニペットを考えてみましょう。

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}
ログイン後にコピー
ログイン後にコピー

検討は終わりましたか?関数の引数をスタックにロードし、「関数を呼び出します」。バイトコードでは、すべての名前がインデックスに変換されます (VM 実行時のインデックス アクセスの方が速いため) が、ここでローカル インデックスを扱っているのかグローバル インデックスを扱っているのかを知る方法はありません。

次に、改良版について考えてみましょう。

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![self.compile_load(name)];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let argc = opcodes.len() - 1;
    opcodes.push(Opcode::Call(argc));

    Ok(opcodes)
}
ログイン後にコピー

そのコードをご検討いただきありがとうございます。

入れ子になった関数呼び出しをサポートするようになりました。何が変わったのでしょうか?

  1. Call オペコードは、関数のインデックスではなく、いくつかの位置引数を取るようになりました。これにより、関数を呼び出す前にスタックからポップオフする引数の数が VM に指示されます。
  2. スタックから引数をポップした後、関数自体はスタック上に残り、compile_load はすでにローカル スコープとグローバル スコープを処理しています。

LOAD_GLOBAL と LOAD_FAST の比較

compile_load が何をしているのか見てみましょう。

fn compile_load(&mut self, name: &str) -> Opcode {
    match self.ensure_context() {
        Context::Global => Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)),
        Context::Local => {
            // Check locals first
            if let Some(index) = self.get_local_index(name) {
                return Opcode::LoadFast(index);
            }

            // If not found locally, fall back to globals
            Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name))
        }
    }
}
ログイン後にコピー

ここではいくつかの重要な原則が機能しています:

  1. 現在のコンテキストに基づいて照合します。 Python セマンティクスに従って、Context::Global は任意のモジュール (スクリプトのエントリポイントだけでなく) のトップレベルにあり、Context::Local は任意のブロック (つまり、関数定義またはクラス定義) 内にあると考えることができます。
  2. ここで、ローカル インデックスと非ローカル インデックスを区別します。 (インデックス 0 がさまざまな場所で何を参照しているかを解読しようとして気が狂ってしまったので、型付き整数を導入しました。LocalIndex と NonlocalIndex は、型指定されていない符号なし整数に型安全性を提供します。これについては将来書くかもしれません!)
  3. 指定された名前のローカル変数が存在するかどうかはバイトコードのコンパイル時にわかります。存在しない場合は、実行時にグローバル変数を検索します。これは、Python に組み込まれたダイナミズムを物語っています。関数の実行時までにそのモジュールのグローバル スコープに変数が存在する限り、その値は実行時に解決できます。ただし、この動的な解像度にはパフォーマンスへの影響が伴います。ローカル変数のルックアップはスタック インデックスを使用するように最適化されていますが、グローバル ルックアップではグローバル名前空間ディクショナリを検索する必要があるため、時間がかかります。この辞書は名前とオブジェクトのマッピングであり、オブジェクト自体がヒープ上に存在する可能性があります。 「グローバルに考え、ローカルに行動する」という格言を誰が知っていたでしょうか。実際には Python スコープを参照していましたか?

変数名には何が含まれますか?

今日最後に説明するのは、これらの変数名がどのようにマップされるかを見てみることです。以下のコード スニペットでは、ローカル インデックスが code.varnames にあり、非ローカル インデックスが code.names にあることがわかります。どちらも、変数と名前のマッピングを含む、Python バイトコードのブロックのメタデータを含む CodeObject 上に存在します。

fn compile_function_call(
    &mut self,
    name: &str,
    args: &ParsedArguments)
) -> Result<Bytecode, CompileError> {
    let mut opcodes = vec![];

    // We push the args onto the stack in reverse call order so that we will pop
    // them off in call order.
    for arg in args.args.iter().rev() {
        opcodes.extend(self.compile_expr(arg)?);
    }

    let (_, index) = self.get_local_index(name);

    // TODO how does this know if it is a global or local index? this may not be the right
    // approach for calling a function
    opcodes.push(Opcode::Call(index));

    Ok(opcodes)
}
ログイン後にコピー
ログイン後にコピー

varname と names の違い (CPython では、これらを co_varnames と co_names と呼びます) に何週間も悩まされましたが、実際には非常に簡単です。 varnames は、指定されたスコープ内のすべてのローカル変数の変数名を保持し、names はすべての非ローカル変数に対して同じことを行います。

これを適切に追跡すると、他のすべては問題なく機能します。実行時に、VM は LOAD_GLOBAL または LOAD_FAST を認識し、それぞれグローバル名前空間ディクショナリまたはローカル スタックを参照することを認識します。

相棒!グーテンベルク氏が電話中で、もう印刷機を保持できないと言っています。

わかりました!大丈夫!わかった!発送しましょう。 ?

メンフィスの次は何でしょうか?

しー!印刷所の人は私が結論を書いていることを知らないので、手短に書きます。

変数のスコープと関数呼び出しがしっかりと確立されたので、スタック トレースや非同期サポートなどの機能に徐々に注意を向けています。バイトコードの詳細を楽しんでいただけた場合、または独自のインタプリタの構築について質問がある場合は、ぜひコメントをお寄せください。


購読して [何もなし] で保存

このような投稿をさらに直接受信トレイに受け取りたい場合は、ここから購読できます!

一緒に働きましょう

私はソフトウェア エンジニアを指導し、協力的な、時にはばかばかしい環境の中で技術的な課題とキャリアの成長を乗り越えていきます。ご興味がございましたら、ここからセッションをご予約ください。

他の場所

私はメンタリングに加えて、自営業や晩期に診断された自閉症を乗り越えた経験についても書いています。コードは減り、ジョークの数は同じです。

  • 湖効果コーヒー、第 2 章 - スクラッチドット組織から

以上がPython バイトコードでネストされた関数のサポートを追加した方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート