如何获取Python异常文本

55

我想在我的C++应用程序中嵌入Python。 我正在使用Boost库 - 一个很好的工具。 但我有一个问题。

如果Python函数引发异常,我想捕获它并在我的应用程序中打印错误或获取一些详细信息,例如导致错误的Python脚本中的行号。

我该怎么做? 我找不到任何在Python API或Boost中获取详细异常信息的函数。

try {
module=import("MyModule"); //this line will throw excetion if MyModule contains an   error
} catch ( error_already_set const & ) {
//Here i can said that i have error, but i cant determine what caused an error
std::cout << "error!" << std::endl;
}

PyErr_Print()只是将错误文本打印到stderr,并清除该错误,因此它不能成为解决方案。

5个回答

65

好的,我找到了如何做到这一点。

不使用boost(只有错误消息,因为从回溯中提取信息的代码太重了,无法在此处发布):

PyObject *ptype, *pvalue, *ptraceback;
PyErr_Fetch(&ptype, &pvalue, &ptraceback);
//pvalue contains error message
//ptraceback contains stack snapshot and many other information
//(see python traceback structure)

//Get error message
char *pStrErrorMessage = PyString_AsString(pvalue);

BOOST版本

try{
//some code that throws an error
}catch(error_already_set &){

    PyObject *ptype, *pvalue, *ptraceback;
    PyErr_Fetch(&ptype, &pvalue, &ptraceback);

    handle<> hType(ptype);
    object extype(hType);
    handle<> hTraceback(ptraceback);
    object traceback(hTraceback);

    //Extract error message
    string strErrorMessage = extract<string>(pvalue);

    //Extract line number (top entry of call stack)
    // if you want to extract another levels of call stack
    // also process traceback.attr("tb_next") recurently
    long lineno = extract<long> (traceback.attr("tb_lineno"));
    string filename = extract<string>(traceback.attr("tb_frame").attr("f_code").attr("co_filename"));
    string funcname = extract<string>(traceback.attr("tb_frame").attr("f_code").attr("co_name"));
... //cleanup here

1
太棒了,这正是我一直在寻找的...运行得非常好。 - Kyle C
1
这很好。我发现在某些情况下(对我来说,例如boost;:python::import导入PYTHONPATH中不存在的内容),ptraceback将为0,因此如果它为0,我会保护不使用ptraceback。另外,您能否评论一下我们可以如何处理extype?我想打印Python异常类型的文本是有意义的。我们该怎么做呢? - D. A.
2
另外一个问题:上面的代码不会有内存泄漏吗?谁来释放 PyErr_Fetch 返回的对象?(我对 CPython 和 boost::python 都不确定) - elmo
我对提取回溯的非boost代码确实很感兴趣。或者只是一些结构的描述,因为我似乎找不到文档。 - solublefish
@elmo 它不会泄漏内存,因为 PyErr_Fetch() 返回对现有对象的引用,而不是分配新对象。不幸的是,用 C 语言知道这一点的唯一方法是阅读源代码,因为它很少有文档记录。这是 Rust 可以解决的问题之一。 - Timmmm
显示剩余2条评论

24

这是我目前能想到的最健壮的方法:

    try {
        ...
    }
    catch (bp::error_already_set) {
        if (PyErr_Occurred()) {
            msg = handle_pyerror(); 
        }
        py_exception = true;
        bp::handle_exception();
        PyErr_Clear();
    }
    if (py_exception) 
    ....


// decode a Python exception into a string
std::string handle_pyerror()
{
    using namespace boost::python;
    using namespace boost;

    PyObject *exc,*val,*tb;
    object formatted_list, formatted;
    PyErr_Fetch(&exc,&val,&tb);
    handle<> hexc(exc),hval(allow_null(val)),htb(allow_null(tb)); 
    object traceback(import("traceback"));
    if (!tb) {
        object format_exception_only(traceback.attr("format_exception_only"));
        formatted_list = format_exception_only(hexc,hval);
    } else {
        object format_exception(traceback.attr("format_exception"));
        formatted_list = format_exception(hexc,hval,htb);
    }
    formatted = str("\n").join(formatted_list);
    return extract<std::string>(formatted);
}

1
显然,将空句柄传递给format_exception是可以的,因此您不需要!tb情况。 - uckelman
1
这个解决方案非常好,但是你需要像这个答案所说的那样调用PyErr_NormalizeException(&exc, &val, &tb); - DJMcMayhem

7
在Python C API中,PyObject_Str会返回一个新的Python字符串对象引用,该字符串对象包含你传递给它的Python对象的字符串形式,就像Python代码中的str(o)一样。请注意,异常对象没有“行号等信息”,这些信息在traceback对象中(你可以使用PyErr_Fetch获取异常对象和traceback对象)。不知道Boost是否提供了使这些特定的C API函数更易于使用的功能,但最坏的情况下,你可以总是使用C API本身提供的这些函数。

非常感谢,Alex。我一直在寻找一种不需要直接调用PyAPI的方法 - 我以为Boost可以处理异常,但是Boost不能 :( - Anton Kiselev
2
@Anton,很高兴我能帮到你,那么你考虑给这个答案点赞并接受它吗?请在此答案的赞数下方使用勾选标记图标(目前为0;-)。 - Alex Martelli
不要忘记使用PyUnicode_AsWideCharString或类似的函数,将从PyObject_Str返回的对象转换为有用的C字符串。 - Mark Ransom

3

这个线程对我非常有用,但是当我尝试提取错误信息本身而不是回溯时,我在Python C API方面遇到了问题。我在Python中找到了很多方法来做到这一点,但我找不到任何在C++中实现这一点的方法。最终,我想出了以下版本,它尽可能地减少了对C API的使用,而更多地依赖于boost python。

PyErr_Print();

using namespace boost::python;

exec("import traceback, sys", mainNamespace_);
auto pyErr = eval("str(sys.last_value)", mainNamespace_);
auto pyStackTrace = eval("'\\n'.join(traceback.format_exception(sys.last_type, sys.last_value, sys.last_traceback))", mainNamespace_);

stackTraceString_ = extract<std::string>(pyStackTrace);
errorSummary_ = extract<std::string>(pyErr);

这个方法能够奏效是因为PyErr_Print()会设置sys.last_valuesys.last_typesys.last_traceback的值,它们的值与sys.exc_info给出的相同,因此这个方法在功能上类似于下面的Python代码:

import traceback
import sys

try:
    raise RuntimeError("This is a test")
except:
    err_type = sys.exc_info()[0]
    value = sys.exc_info()[1]
    tb = sys.exc_info()[2]

    stack_trace = "\n".join(traceback.format_exception(err_type, value, tb))
    error_summary = str(value)


print(stack_trace)
print(error_summary)

我希望有人能从中受益!


1
你如何在这里初始化mainNamespace_? - aCuria
1
对我来说,import("main").attr("dict") 似乎是解决问题的方法。 - Daniel Hesslow
谢谢,这对我有用:https://gitlab.com/yade-dev/trunk/-/merge_requests/581 - Janek_Kozicki

2

这是一些基于其他答案和评论的代码,用现代化的C++编写并进行了良好的格式化和注释。经过最少的测试,但似乎可以正常工作。

#include <string>
#include <boost/python.hpp>
#include <Python.h>

// Return the current Python error and backtrace as a string, or throw
// an exception if there was none.
std::string python_error_string() {
  using namespace boost::python;

  PyObject* ptype = nullptr;
  PyObject* pvalue = nullptr;
  PyObject* ptraceback = nullptr;

  // Fetch the exception information. If there was no error ptype will be set
  // to null. The other two values might set to null anyway.
  PyErr_Fetch(&ptype, &pvalue, &ptraceback);
  if (ptype == nullptr) {
    throw std::runtime_error("A Python error was detected but when we called "
                             "PyErr_Fetch() it returned null indicating that "
                             "there was no error.");
  }

  // Sometimes pvalue is not an instance of ptype. This converts it. It's
  // done lazily for performance reasons.
  PyErr_NormalizeException(&ptype, &pvalue, &ptraceback);
  if (ptraceback != nullptr) {
    PyException_SetTraceback(pvalue, ptraceback);
  }

  // Get Boost handles to the Python objects so we get an easier API.
  handle<> htype(ptype);
  handle<> hvalue(allow_null(pvalue));
  handle<> htraceback(allow_null(ptraceback));

  // Import the `traceback` module and use it to format the exception.
  object traceback = import("traceback");
  object format_exception = traceback.attr("format_exception");
  object formatted_list = format_exception(htype, hvalue, htraceback);
  object formatted = str("\n").join(formatted_list);
  return extract<std::string>(formatted);
}

顺便问一下,为什么大家都使用handle<>而不是handle?显然这会禁用模板参数推导。我不确定为什么你要在这里这样做,但无论如何它并不相同,Boost文档也建议使用handle<>,所以我想肯定有一个很好的理由。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接