如何预初始化 Python 解释器并保留脚本 traceback 行号?
结论
不要逐行把用户脚本写入非交互模式 Python 进程的 stdin。可以让子进程运行固定的 worker 程序:先初始化 C# 环境,再从 stdin 读取脚本路径,最后通过 runpy.run_path() 执行脚本文件。这样 Python 会按真实文件编译脚本,异常 traceback 也会包含对应的文件名和行号。
例如,worker 可以这样写:
# worker.py
import json
import runpy
import sys
import traceback
def init_csharp():
# 在这里加载并初始化 C# / pythonnet 基础设施
pass
init_csharp()
# 初始化完成后再读取任务。父进程也可以在初始化期间先写入路径,
# 这条输入会留在管道中,等 worker 读到时初始化已经完成。
request = json.loads(sys.stdin.readline())
script_path = request["path"]
try:
runpy.run_path(script_path, run_name="__main__")
except BaseException:
traceback.print_exc()
父进程传递脚本路径,而不是脚本源码:
import json
import subprocess
import sys
proc = subprocess.Popen(
[sys.executable, "-u", "worker.py"],
stdin=subprocess.PIPE,
text=True,
)
proc.stdin.write(json.dumps({"path": "/path/to/user_script.py"}) + "\n")
proc.stdin.flush()
proc.stdin.close()
proc.wait()
-u 会让 Python 的标准流使用无缓冲模式。这个示例没有捕获 stdout 和 stderr,因此即使用户脚本输出较多,也不会因为父进程没有及时读取管道而卡住。若要捕获输出,需要持续读取这些管道,或将输出重定向到文件。
为什么逐行写入 stdin 会延迟执行
Python 通常会把从管道读取的标准输入当作脚本内容处理,而不会像终端交互会话那样,每收到一行就执行一行。父进程持续写入时,解释器可能会继续等待;stdin 关闭后,解释器才知道输入结束,并执行收到的源码。-i 会影响脚本结束后的交互行为,但无法让管道成为可靠的逐行任务协议。
逐行发送多行脚本还会丢失原始文件上下文。Python 收到的是 stdin 中的一段输入,不是按用户脚本路径编译的文件,因此 traceback 无法可靠地显示原脚本的文件名和行号。
保留 traceback 信息
runpy.run_path(script_path, run_name="__main__") 会按脚本路径执行文件,并将运行名称设为 __main__。如果异常未被捕获,Python 会正常输出 traceback;上面的代码捕获异常后调用 traceback.print_exc(),显示的仍是原脚本中的异常位置。
如果脚本依赖命令行参数,执行前需要设置 sys.argv,例如:
sys.argv = [script_path, "--option", "value"]
runpy.run_path(script_path, run_name="__main__")
如果必须通过管道传递源码,而不能传递文件路径,也可以显式指定文件名来编译:
source = sys.stdin.read()
code = compile(source, "/path/to/user_script.py", "exec")
exec(code, {"__name__": "__main__", "__file__": "/path/to/user_script.py"})
指定的文件名会出现在 traceback 中,但源码仍要完整读入后才能编译执行。对于已经存在的脚本文件,直接使用 runpy.run_path() 更简单。
复用解释器时的注意事项
如果 worker 会连续执行多个脚本,需要明确任务边界和错误处理方式。Python 进程中的模块缓存、全局状态和 UI 事件循环也可能在任务间保留。如果每个任务都需要相互隔离,执行完一个脚本后退出进程,再从池中取一个已初始化的新进程会更稳妥。使用 PyQt 或 Tk 时,还要确保 UI 操作符合对应框架的线程和事件循环要求。
备注:内容仅供参考。
