在 PyGetSetDef 中使用闭包来重用属性 getter-settter

Use closure in PyGetSetDef for reusing attribute getter-settter

如标题所示,我正在尝试创建一堆属性,但代码变得重复且混乱。我想使用 closure 参数使代码更紧凑。

根据C API reference,闭包是一个函数指针,为getters/setters提供额外的信息。我没能找到它的使用示例。

这就是我目前的使用方式:

static void closure_1() {};
static void closure_2() {};
...
static PyObject *
FOO_getter(FOO* self, void *closure) {
    if (closure == &closure_1) {
        return self->bar_1;
    } else if (closure == &closure_2) {
        return self->bar_2;
    }
}
static int
FOO_setter(FOO* self, PyObject *value, void *closure) {
    if (closure == &closure_1) {
        if (somehow value is invalid) {
            PyErr_SetString(PyExc_ValueError, "invalid value for bar_1.");
            return -1;
        }
    } else if (closure == closure_2) {
        if (somehow value is invalid) {
            PyErr_SetString(PyExc_ValueError, "invalid value for bar_2.");
            return -1;
        }
    }
    return 0;
}
static PyGetSetDef FOO_getsetters[] = {
    {"bar_1", (getter) FOO_getter, (setter) FOO_setter, "bar_1 attribute", closure_1},
    {"bar_2", (getter) FOO_getter, (setter) FOO_setter, "bar_2 attribute", closure_2},
    {NULL}  /* Sentinel */
};
...

它按照我想要的方式工作,但它看起来更像是 hack 而不是“pythonic”。有没有更好的方法来处理这个问题?例如以某种方式调用闭包。

尽管有文档,我假设 closure 可以是您想要的任何指针。那么传递一个“对象”怎么样,因为 C 不支持闭包(缺少 literally generating functions at run-time)。

在一个对象中,我们可以在FOO中存储成员的偏移量,以及指向特定于属性的验证器的指针。

typedef int (*Validator)(FOO *, const struct Attribute *, void *);

typedef struct Attribute {
   const char *name;
   size_t offset;
   Validator validator;
} Attribute;

static PyObject **resolve_offset(FOO *self, const Attribute *attr) {
   return (PyObject **)( ( (char *)self ) + attr->offset );
}

static PyObject *FOO_getter(FOO *self, void *_attr) {
   const Attribute *attr = (const Attribute *)_attr;
   return *resolve_offset(self, attr);
}

static int FOO_setter(FOO *self, PyObject *val, void *_attr) {
   const Attribute *attr = (const Attribute *)_attr;
   if (attr->validator(self, attr, val)) {
      *resolve_offset(self, attr) = val;
      return 0;
   } else {
      // Building the string to include attr->name is left to you.
      PyErr_SetString(PyExc_ValueError, "invalid value.");
      return -1;
   }
}

static int FOO_bar_1_validator(FOO *self, const Attribute *attr, void *val) { ... }
static int FOO_bar_2_validator(FOO *self, const Attribute *attr, void *val) { ... }

#define ATTRIBUTE(name)                      \
   static Attribute FOO_ ## name ## attr = { \
      #name,                                 \
      offsetof(FOO, name),                   \
      FOO_ ## name ## _validator             \
   };

ATTRIBUTE(bar_1);
ATTRIBUTE(bar_2);

#define PY_ATTR_DEF(name) { \
   #name,                   \
   (getter)FOO_getter,      \
   (setter)FOO_setter,      \
   #name " attribute",      \
   &(FOO_ ## name ## attr)  \
}

static PyGetSetDef FOO_getsetters[] = {
    PY_ATTR_DEF(bar_1),
    PY_ATTR_DEF(bar_2),
    { NULL }
};

我最初写道:

resolve_offset surely relies on undefined behaviour, but it should work fine. The alternative would be to have three functions in our attribute object (get, validate, set) instead of one, but that defies the point of the question.

但是@tsanisl 指出它看起来像。太棒了!

我猜这个“闭包”是用来将额外的上下文传递给 Foo_getter。它应该可以简化 Foo 成员的访问。文档很可能是错误的。应该是“可选指针”,而不是“可选函数指针”。

考虑传递成员的偏移量。使用 stddef.h 中定义的标准 offsetof 宏可以轻松获得结构成员的偏移量。它是一个小的无符号整数,适合 void* 类型。

static PyGetSetDef FOO_getsetters[] = {
    {"bar_1", (getter) FOO_getter, (setter) FOO_setter, "bar_1 attribute", (void*)offsetof(FOO, bar_1)},
    {"bar_2", (getter) FOO_getter, (setter) FOO_setter, "bar_2 attribute", (void*)offsetof(FOO, bar_2)},
    {NULL}  /* Sentinel */
};

现在 getter 可能是:

static PyObject *
FOO_getter(FOO* self, void *closure) {
    // pointer to location where the FOO's member is stored
    char *memb_ptr = (char*)self + (size_t)closure;
    // cast to `PyObject**` because `mem_ptr` points to location where a pointer to `PyObject` is stored
    return *(PyObject**)mem_ptr;
}

对 setter 使用类似的架构。