You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Pyodide中实现Python代码的分步调试执行?

在Pyodide中实现分步调试(单步执行/断点/变量检查)

分步执行完全可行,核心思路是借助Python内置的调试跟踪机制(sys.settrace 或 pdb 模块),结合Pyodide的JS-Python交互能力,将调试控制权交还给前端UI,实现单步、断点、变量检查等功能。以下是具体实现方案:

方案一:自定义跟踪函数(灵活度高,适合教学场景)

通过sys.settrace注册自定义跟踪函数,捕获代码执行的行事件、函数调用/返回事件,在需要暂停的节点(断点、单步指令)将状态同步到前端,等待用户操作后继续执行。

1. Python端核心代码

import sys
import asyncio
from pyodide import to_js

# 调试状态存储,供JS端修改控制
debug_state = {
    "breakpoints": set(),  # 存储断点行号
    "current_action": "continue",  # 控制指令:continue/step_in/step_over/step_out
    "is_paused": False,
    "current_frame": None
}

def trace_handler(frame, event, arg):
    global debug_state
    if event == "line":
        line_no = frame.f_lineno
        # 触发暂停条件:到达断点 或 当前指令为单步
        if line_no in debug_state["breakpoints"] or debug_state["current_action"] in ["step_in", "step_over"]:
            debug_state["current_frame"] = frame
            # 将当前调试状态同步到前端
            js.debugger.onPause(to_js({
                "type": "line",
                "line": line_no,
                "file": frame.f_code.co_filename,
                "locals": to_js(frame.f_locals),
                "globals": to_js(frame.f_globals)
            }))
            # 等待前端指令
            while debug_state["is_paused"]:
                asyncio.sleep(0.01)  # 让出Pyodide事件循环控制权
            # 单步指令执行后重置
            if debug_state["current_action"] in ["step_in", "step_over"]:
                debug_state["current_action"] = "continue"

    elif event == "call":
        # 单步进入函数时暂停
        if debug_state["current_action"] == "step_in":
            debug_state["current_frame"] = frame
            js.debugger.onPause(to_js({
                "type": "call",
                "function": frame.f_code.co_name,
                "locals": to_js(frame.f_locals)
            }))
            while debug_state["is_paused"]:
                asyncio.sleep(0.01)

    elif event == "return":
        # 单步跳出函数时暂停
        if debug_state["current_action"] == "step_out":
            debug_state["current_frame"] = frame
            js.debugger.onPause(to_js({
                "type": "return",
                "function": frame.f_code.co_name,
                "return_value": arg
            }))
            while debug_state["is_paused"]:
                asyncio.sleep(0.01)
            debug_state["current_action"] = "continue"

    return trace_handler

# 注册跟踪函数
sys.settrace(trace_handler)

2. JS端控制逻辑

通过Pyodide全局对象修改Python端的debug_state,响应用户操作(单步、继续、添加断点):

// 前端调试器控制对象
window.debugger = {
    // 接收Python端同步的暂停状态
    onPause: function(state) {
        // 在这里更新UI:显示当前行号、变量列表等
        console.log(`暂停于行 ${state.line}`);
        console.log("局部变量:", state.locals);
        
        // 绑定按钮事件(示例)
        document.getElementById("step-over-btn").onclick = () => {
            pyodide.globals.get("debug_state").set("current_action", "step_over");
            pyodide.globals.get("debug_state").set("is_paused", false);
        };
        document.getElementById("continue-btn").onclick = () => {
            pyodide.globals.get("debug_state").set("current_action", "continue");
            pyodide.globals.get("debug_state").set("is_paused", false);
        };
        document.getElementById("step-in-btn").onclick = () => {
            pyodide.globals.get("debug_state").set("current_action", "step_in");
            pyodide.globals.get("debug_state").set("is_paused", false);
        };
        document.getElementById("step-out-btn").onclick = () => {
            pyodide.globals.get("debug_state").set("current_action", "step_out");
            pyodide.globals.get("debug_state").set("is_paused", false);
        };

        // 设置暂停标记,等待用户操作
        pyodide.globals.get("debug_state").set("is_paused", true);
    },
    // 添加断点
    addBreakpoint: function(lineNo) {
        pyodide.globals.get("debug_state").get("breakpoints").add(lineNo);
    }
};

// 异步运行目标脚本(必须包装为异步函数,避免阻塞事件循环)
async function runDebugScript(script) {
    await pyodide.runAsync(`
        import asyncio
        async def execute_script():
            ${script}
        asyncio.run(execute_script())
    `);
}

方案二:基于pdb模块(复用Python原生调试能力)

如果想直接复用Python的pdb调试命令(如n单步跳过、s单步进入),可以重定向sys.stdin/stdout,将pdb的输入输出与前端UI绑定。

核心代码示例

import sys
from pyodide.ffi import create_proxy

# 自定义IO类,对接前端
class PDBIO:
    def readline(self):
        # 等待前端输入调试命令
        return js.debugger.getPDBCommand()
    def write(self, data):
        # 将pdb输出发送到前端
        js.debugger.showPDBOutput(data)

# 替换标准IO
sys.stdin = PDBIO()
sys.stdout = PDBIO()

# 启动pdb调试
import pdb
target_script = """
def add(a, b):
    return a + b

x = 10
y = 20
z = add(x, y)
print(z)
"""
pdb.run(target_script)

JS端实现getPDBCommand和showPDBOutput,比如通过输入框获取用户命令,通过控制台或UI区域显示输出。

关键注意事项

  1. 异步执行:必须将目标脚本包装为异步函数并通过pyodide.runAsync执行,否则跟踪函数中的asyncio.sleep会阻塞Pyodide事件循环。
  2. 变量序列化:使用pyodide.to_js转换Python变量时,复杂对象(如自定义类)需要手动处理__dict__属性来实现序列化。
  3. 性能优化:逐行跟踪会降低代码执行速度,教学场景下可接受,但需避免在大规模脚本中使用。
  4. 作用域处理:跟踪函数中的frame.f_locals和frame.f_globals分别对应当前执行的局部和全局作用域,可用于展示变量。

内容的提问来源于stack exchange,提问作者tglas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 16:57:34