如果你真的需要欺骗另一个库——尤其是用 C 语言编写并使用非公共 API 的库——有两种可能的方法来获取真正的回溯对象。我还没有让任何一个可靠地工作。此外,两者都是 CPython 特定的,不仅需要使用 C API 层,还需要使用可能随时更改的未记录类型和函数,并为解释器提供新的和令人兴奋的机会来进行分段错误。但是,如果您想尝试,它们可能对您有用。
PyTraceBack 类型不是公共 API 的一部分。但是(除了在 Python 目录而不是 Object 目录中定义)它是作为 C API 类型构建的,只是没有记录。因此,如果您查看您的 Python 版本的 traceback.h 和 traceback.c,您会发现……嗯,没有 PyTraceBack_New,但是有 PyTraceBack_Here 构造了一个新的回溯并将其交换到当前的异常信息中。我不确定调用它是否有效,除非当前存在异常,并且如果存在 is 当前异常,您可能会通过像这样对其进行变异来搞砸它,但需要进行一些试验和崩溃或阅读代码,希望你可以让它工作:
import ctypes
import sys
ctypes.pythonapi.PyTraceBack_Here.argtypes = (ctypes.py_object,)
ctypes.pythonapi.PyTraceBack_Here.restype = ctypes.c_int
def _fake_tb():
try:
1/0
except:
frame = sys._getframe(2)
if ctypes.pythonapi.PyTraceBack_Here(frame):
raise RuntimeError('Oops, probably hosed the interpreter')
raise
def get_tb():
try:
_fake_tb()
except ZeroDivisionError as e:
return e.__traceback__
作为一个有趣的替代方案,我们可以尝试动态改变回溯对象。要获取回溯对象,只需引发并捕获异常:
try: 1/0
except exception as e: tb = e.__traceback__ # or sys.exc_info()[2]
唯一的问题是它指向你的堆栈帧,而不是你的调用者的,对吧?如果回溯是可变的,您可以轻松解决:
tb.tb_lasti, tb.tb_lineno = tb.tb_frame.f_lasti, tb.tb_frame.f_lineno
tb.tb_frame = tb.tb_frame.f_back
也没有设置这些东西的方法。请注意,它没有setattro,它的getattro 是通过动态构建__dict__ 来工作的,所以显然我们获得这些东西的唯一方法是通过底层结构。您应该使用 ctypes.Structure 真正构建它,但作为快速破解:
p8 = ctypes.cast(id(tb), ctypes.POINTER(ctypes.c_ulong))
p4 = ctypes.cast(id(tb), ctypes.POINTER(ctypes.c_uint))
现在,对于普通的 64 位 CPython 构建,p8[:2] / p4[:4] 是普通的对象头,然后是特定于回溯的字段,所以 p8[3] 是 tb_frame,而 @ 987654341@ 和 p4[9] 分别是 tb_lasti 和 tb_lineno。所以:
p4[8], p4[9] = tb.tb_frame.f_lasti, tb.tb_frame.f_lineno
但是下一部分有点难,因为tb_frame 实际上不是PyObject *,它只是一个原始的struct _frame *,所以你转到frameobject.h,在那里你会发现它确实是一个PyFrameObject * 所以你可以再次使用相同的技巧。在重新分配p8[3] 以指向pf8[3] 后,请记住_ctypes.Py_INCREF 帧的下一帧和Py_DECREF 帧本身,或者一旦您尝试打印回溯,您就会出现段错误并丢失所有工作已经写完了。 :)