首頁 > 後端開發 > Python教學 > 我如何在 Python 字節碼中添加對嵌套函數的支持

我如何在 Python 字節碼中添加對嵌套函數的支持

Susan Sarandon
發布: 2024-12-31 18:58:18
原創
885 人瀏覽過

How I added support for nested functions in Python bytecode

我想分享一些非常酷的東西我一直在和你學習Python字節碼,包括我如何添加對嵌套的支援函數,但我的印刷工說我需要將其控制在500 字以內。

這是一個假期週,他聳聳肩。 你希望我做什麼?

不包含程式碼片段,我討價還價了。

好吧,他讓出。

你知道我們為什麼要使用字節碼嗎?

我只是操作印刷機,不過我相信你。

很公平。讓我們開始吧。

為什麼我們先使用字節碼

Memphis,我用 Rust 寫的 Python 解釋器,有兩個執行引擎。兩者都不能運行所有程式碼,但都可以運行部分程式碼。

我的treewalk 解釋器 是您在不知道自己在做什麼的情況下建造的。 ?‍♂️ 將輸入的 Python 程式碼標記,產生抽象語法樹 (AST),然後遍歷樹並評估每個節點。表達式傳回值和語句修改符號表,符號表實作為一系列遵守 Python 作用域規則的作用域。只要記住簡單的肺炎 LEGB:局部、封閉、全域、內建。

我的字節碼虛擬機是如果你不知道自己在做什麼但想像你一樣行事時你會建構的。還有? ‍♂️。對於這個引擎,令牌和 AST 的工作方式相同,但我們不是步行,而是衝刺。我們將 AST 編譯為中間表示(IR),以下稱為字節碼。然後,我們建立一個基於堆疊的虛擬機器 (VM),它在概念上類似於 CPU,按順序執行字節碼指令,但它完全由軟體實作。

(對於這兩種方法的完整指南,沒有漫無目的,《Crafting Interpreters》非常好。)

我們首先為什麼要這樣做?只要記住兩個 P:便攜性和性能。還記得在 2000 年代初期,沒有人會對 Java 字節碼的可移植性閉嘴嗎? 您只需要一個 JVM,就可以執行在任何機器上編譯的 Java 程式! 出於技術和行銷原因,Python 選擇不採用這種方法,但理論上同樣的原則適用。 (實際上,編譯步驟是不同的,我很遺憾打開了這罐蠕蟲。)

性能才是最重要的。編譯後的 IR 是一種更有效的表示形式,而不是在程式的生命週期內多次遍歷 AST。我們看到,由於避免了重複遍歷 AST 的開銷,效能得到了提高,而且其扁平結構通常會在運行時帶來更好的分支預測和快取局部性。

(如果你沒有電腦架構背景,我不會責怪你沒有考慮快取——哎呀,我的職業生涯是從這個行業開始的,我對緩存的考慮遠遠少於我對如何避免的考慮編寫同一行程式碼兩次。

嘿夥計,這是 500 字。我們需要加載框架並讓其撕裂。

已經? !您排除了程式碼片段嗎?

沒有程式碼片段,老兄。

好吧好吧。只剩下500多了。我保證。

上下文對於 Python 變數很重要

大約一年前,在提交我的字節碼 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)
}
登入後複製
登入後複製
你考慮好了嗎?我們將函數參數載入到堆疊上並“呼叫函數”。在字節碼中,所有名稱都會轉換為索引(因為在虛擬機器運行時索引存取速度更快),但我們實際上沒有辦法知道我們在這裡處理的是本機索引還是全域索引。

現在考慮改進版本。


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)
}
登入後複製
感謝您考慮程式碼。

我們現在支援巢狀函數呼叫!發生了什麼變化?

    Call 操作碼現在採用多個位置參數,而不是函數的索引。這指示 VM 在呼叫函數之前從堆疊中彈出多少個參數。
  1. 將參數從堆疊中彈出後,
  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 作用域?

varname 中有什麼?

今天我要留給您的最後一件事是看看這些變數名稱是如何映射的。在下面的程式碼片段中,您會注意到在 code.varnames 中找到本機索引,在 code.names 中找到非本地索引。兩者都存在於 CodeObject 上,其中包含 Python 字節碼塊的元數據,包括其變數和名稱映射。

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)
}
登入後複製
登入後複製

varnames 和 name 之間的差異折磨了我好幾個星期(CPython 稱這些為 co_varnames 和 co_names),但它實際上相當簡單。 varnames 保存給定範圍內所有局部變數的變​​數名稱,names 保存所有非局部變數的變​​數名稱。

一旦我們正確追蹤這一點,其他一切都會正常工作。在運行時,VM 會看到 LOAD_GLOBAL 或 LOAD_FAST,並知道分別查看全域命名空間字典或本機堆疊。

老兄!古騰堡先生打電話說我們不能再按了。

好的!美好的!我得到它!我們發貨吧。 ?

孟菲斯的下一步是什麼?

噓!印刷工不知道我在寫結論,所以我會簡短地說。

透過固定的變數作用域和函數調用,我逐漸將注意力轉向堆疊追蹤和非同步支援等功能。如果您喜歡深入了解字節碼或對構建自己的解釋器有疑問,我很樂意聽取您的意見 - 發表評論!


訂閱並節省[無任何]

如果您想將更多類似的貼文直接發送到您的收件匣,您可以在這裡訂閱!

跟我一起工作

我指導軟體工程師在一個有時有些愚蠢的支持性環境中應對技術挑戰和職業發展。如果您有興趣,可以在這裡預約課程。

其他地方

除了指導之外,我還寫了我在自營職業和晚期診斷自閉症方面的經驗。更少的程式碼和相同數量的笑話。

  • 湖效應咖啡,第 2 章 - 來自 Scratch dot org

以上是我如何在 Python 字節碼中添加對嵌套函數的支持的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板