如何在自定义 PyTypeObject 中同时使用 .tp_getattro / .tp_setattro 和 .tp_getset?

How does one use both .tp_getattro / .tp_setattro and .tp_getset in a custom PyTypeObject?

我正在努力将现有的 C 应用程序扩展到包含自定义数据类型的 Python。我希望用户能够将任意字符串作为此自定义类型对象的属性传递。一些特殊属性将始终存在并具有已知行为(例如 CustomObject()._ndim)。对象中可能存在也可能不存在任何其他字符串,并且需要额外处理(例如 CustomObject().asdf)。

为了解决这个问题并尽可能接近标准 API,我想利用 .tp_getattro / .tp_setattro 和 [= PyObjectType 结构的 19=] 特征。 .tp_getset 为定义 getter 和 setter、文档提供了一个干净的界面,并自动在 dir() 中结束。 .tp_getattro / .tp_setattro 是提供动态功能所必需的。

但是,如果两者都作为 PyTypeObject 实例提供,则生成的类型将忽略 .tp_getset 函数,因为 .tp_getattro / .tp_setattro 已被覆盖。我假设有一个调用或短节我需要添加到我的 .tp_getattro / .tp_setattro 实现中才能完成这项工作。

到目前为止,当我的 *attro() 覆盖被调用时,我已经尝试通过 GDB 返回回溯。我看到调用堆栈中有一个拆分,具体取决于实现的是哪个,这表明涉及 PyObject_GenericGetAttr()

.tp_getset 已实施

[0] from 0x00007ffff74700d6 in Info_get_ndim+17 at info.c:396
[1] from 0x00000000004c4419 in getset_get+185 at ../Objects/typeobject.c:1399
[2] from 0x000000000059b20f in _PyObject_GenericGetAttrWithDict+730 at ../Objects/typeobject.c:3125
[3] from 0x000000000059b20f in PyObject_GenericGetAttr+730 at ../Objects/object.c:1306
[4] from 0x000000000059b20f in PyObject_GetAttr+799 at ../Objects/object.c:912
[5] from 0x0000000000541fb5 in _PyEval_EvalFrameDefault+1077 at ../Python/ceval.c:2573
# ...

.tp_getattro 已实施

[0] from 0x00007ffff74700d6 in Info_getattro+17 at info.c:185
[1] from 0x000000000059b20f in PyObject_GetAttr+434 at ../Objects/dictobject.c:1333
[2] from 0x0000000000541fb5 in _PyEval_EvalFrameDefault+1077 at ../Python/ceval.c:2573
# ...

如何在自定义 PyTypeObject 中同时使用 .tp_getattro / .tp_setattro.tp_getset

谢谢!

感谢@DavidW 在对问题的评论中,我找到了一个解决方案:分别是 .tp_getattro / .tp_setattro functions must call PyObject_GenericGetAttr() / PyObject_GenericSetAttr()

为什么这不会自动发生是有道理的:现在您有了更多的控制权。您可以控制要优先使用哪些属性,如果属性不存在该怎么做。就我而言,我希望动态属性具有优先权,并且我不希望用户能够添加自己的属性。所以我的 *attro() 看起来类似于:

Custom_getattro()

static PyObject * Custom_getattro(PyObject * o, PyObject * attr_name) {
  CustomObject * s = (CustomObject *) o;
  PyObject * res, * type, * value, * traceback;

  // First try to get a dynamic attribute
  res = _get_custom(s, attr_name);

  // If that was unsuccessful, get an attribute out of our .tp_dict
  if (! res) {
    PyErr_Fetch(&type, &value, &traceback);
    res = PyObject_GenericGetAttr(o, attr_name);

    // Use the original error, if necessary
    if (! res) {
      PyErr_Restore(type, value, traceback);
    } else {
      Py_XDECREF(type);
      Py_XDECREF(value);
      Py_XDECREF(traceback);
    }
  }

  return res;
}

Custom_setattro()

static int32_t Custom_setattro(PyObject * o, PyObject * attr_name, PyObject * v) {
  int32_t rc = -1;
  CustomObject * s;
  PyObject * type, * value, * traceback;

  // First try to set a dynamic attribute.
  rc = _set_custom(o, attr_name);

  // If that was unsuccessful, set an attribute in our .tp_dict
  if (rc != 0) {
    PyErr_Fetch(&type, &value, &traceback);
    rc = PyObject_GenericSetAttr(o, attr_name, v);

    // Use the original error, if necessary
    if (rc != 0) {
      PyErr_Restore(type, value, traceback);
    } else {
      Py_XDECREF(type);
      Py_XDECREF(value);
      Py_XDECREF(traceback);
    }
  }

  return rc;
}