具有递归的 Python C API - 段错误

Posted

技术标签:

【中文标题】具有递归的 Python C API - 段错误【英文标题】:Python C API with recursion - segfaults 【发布时间】:2012-10-31 14:22:24 【问题描述】:

我在 C++ 中使用 python 的 C API (2.7) 将 python 树结构转换为 C++ 树。代码如下:

python 树以递归方式实现为具有子列表的类。叶节点只是原始整数(不是类实例)

我加载一个模块并从 C++ 调用一个 python 方法,使用来自 here 的代码,它返回树的一个实例,python_tree,作为 C++ 中的 PyObject。

递归遍历得到的PyObject。要获取孩子的列表,我这样做:

PyObject* attr = PyString_FromString("children");
PyObject* list = PyObject_GetAttr(python_tree,attr);
for (int i=0; i<PyList_Size(list); i++) 
    PyObject* child = PyList_GetItem(list,i); 
    ...

非常简单,它可以工作,直到我最终在调用 PyObject_GetAttr 时遇到分段错误(Objects/object.c:1193,但我看不到 API 代码)。它似乎发生在访问树的 last 叶节点时。

我很难确定问题所在。使用 C API 进行递归是否有任何特殊注意事项?我不确定我是否需要使用 Py_INCREF/Py_DECREF,或者使用 these 函数或其他东西。老实说,我并不完全理解 API 的工作原理。非常感谢任何帮助!

编辑:一些最小的代码:

void VisitTree(PyObject* py_tree) throw (Python_exception)

    PyObject* attr = PyString_FromString("children");
    if (PyObject_HasAttr(py_tree, attr)) // segfault on last visit
    
        PyObject* list = PyObject_GetAttr(py_tree,attr);
        if (list)
        
            int size = PyList_Size(list);
            for (int i=0; i<size; i++)
            
                PyObject* py_child = PyList_GetItem(list,i);
                PyObject *cls = PyString_FromString("ExpressionTree");
                // check if child is class instance or number (terminal)
                if (PyInt_Check(py_child) || PyLong_Check(py_child) || PyString_Check(py_child)) 
                    ;// terminal - do nothing for now
                else if (PyObject_IsInstance(py_child, cls))
                    VisitTree(py_child);
                else
                    throw Python_exception("unrecognized object from python");
            
        
    

【问题讨论】:

你检查了,所以你没有 NULL 指针? 你能显示更多代码吗?也许您没有正确检查 NULLs 或叶节点。 PyObject_GetAttr 返回一个新实例,因此您不必Py_INCREF 它,但请务必检查返回的值是否为NULL(表示失败)。 PyList_GetItem 返回一个借来的引用,所以你应该 Py_DECREF它。如果您必须存储其结果以供将来使用,您必须使用 Py_INCREF 获取引用的所有权(但我认为您不必这样做)。 我在上面添加了一些代码。我已经进入调试模式并确保没有空指针。无法在 API 内调试。我被困住了。 【参考方案1】:

您可以识别 Python/C 代码的几个问题:

PyObject_IsInstance 将类而不是字符串作为其第二个参数。

没有专用于reference counting 的代码。新的引用,例如PyObject_GetAttr 返回的引用,永远不会被释放,而通过PyList_GetItem 获得的借用引用永远不会是acquired before use。将 C++ 异常与其他纯 Python/C 混合使用会加剧问题,使得实现正确的引用计数变得更加困难。

缺少重要的错误检查。 PyString_FromString 内存不足时会失败;如果列表同时缩小,PyList_GetItem 可能会失败;即使在PyObject_HasAttr 成功之后,PyObject_GetAttr 在某些情况下也会失败。

这是代码的重写(但未经测试)版本,具有以下更改:

实用函数GetExpressionTreeClass 从定义它的模块中获得ExpressionTree 类。 (为my_module填写正确的模块名称。)

Guard 是一个RAII 风格的保护类,它在离开作用域时释放 Python 对象。这个小而简单的类使引用计数异常安全,它的构造函数自己处理 NULL 对象。 boost::python 定义了这种风格的功能层,我建议看看它。

所有Python_exception throws 现在都附有设置 Python 异常信息。因此Python_exception 的捕手可以使用PyErr_PrintExcPyErr_Fetch 打印异常或找出问题所在。

代码:

class Guard 
  PyObject *obj;
public:
  Guard(PyObject *obj_): obj(obj_) 
    if (!obj)
      throw Python_exception("NULL object");
  
  ~Guard() 
    Py_DECREF(obj);
  
;

PyObject *GetExpressionTreeClass()

  PyObject *module = PyImport_ImportModule("my_module");
  Guard module_guard(module);
  return PyObject_GetAttrString(module, "ExpressionTree");


void VisitTree(PyObject* py_tree) throw (Python_exception)

  PyObject *cls = GetExpressionTreeClass();
  Guard cls_guard(cls);

  PyObject* list = PyObject_GetAttrString(py_tree, "children");
  if (!list && PyErr_ExceptionMatches(PyExc_AttributeError)) 
    PyErr_Clear();  // hasattr does this exact check
    return;
  
  Guard list_guard(list);

  Py_ssize_t size = PyList_Size(list);
  for (Py_ssize_t i = 0; i < size; i++) 
    PyObject* child = PyList_GetItem(list, i);
    Py_XINCREF(child);
    Guard child_guard(child);

    // check if child is class instance or number (terminal)
    if (PyInt_Check(child) || PyLong_Check(child) || PyString_Check(child)) 
      ; // terminal - do nothing for now
    else if (PyObject_IsInstance(child, cls))
      VisitTree(child);
    else 
      PyErr_Format(PyExc_TypeError, "unrecognized %s object", Py_TYPE(child)->tp_name);
      throw Python_exception("unrecognized object from python");
    
  

【讨论】:

谢谢,我非常感谢细节。我已经应用了你的更改,现在我在PyObject_IsInstance(child, cls) 遇到了段错误。有没有办法调试 python 的 C API 代码?来源是否可用?再次感谢。 我注意到,如果我在稍微不同的树上运行它,它甚至不会通过函数调用从 python 获取树(甚至没有到达我们的代码)在这里重新谈论)。 PyObject_Call 中发生段错误。让我想知道我的 python-dev 安装是否搞砸了。我似乎同时安装了 2.6 和 2.7。 Python/C API 的源代码是 Python 本身的源代码。如果你在 Unix 上运行,你可以解压它并用 ./configuremake 编译它。 也许您应该摆脱py_embed 库,直接使用Python/C API?这并不那么难。 是的,我只是想要一些入门代码。我发现this tutorial 更成功。

以上是关于具有递归的 Python C API - 段错误的主要内容,如果未能解决你的问题,请参考以下文章

使用 python-c-api 调用的 Cython 回调段错误

扫雷艇。递归检查的段错误

C语言调用函数发生段错误,发生段错误的函数是没有执行就段错误了

递归 段错误 习题

为啥无限递归会导致段错误

Numpy C API - 使用 PyArray_Descr 创建数组会导致段错误