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

Posted

技术标签:

【中文标题】在 PyGetSetDef 中使用闭包来重用属性 getter-setter【英文标题】:Use closure in PyGetSetDef for reusing attribute getter-settter 【发布时间】:2021-12-28 15:02:29 【问题描述】:

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

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

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

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”。有没有更好的方法来处理这个?例如以某种方式调用闭包。

【问题讨论】:

【参考方案1】:

我猜这个“闭包”是用来将额外的上下文传递给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 */
;

现在吸气剂可能是:

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 使用类似的架构。

【讨论】:

相当整洁。闭包是任何类型的指针似乎都是合乎逻辑的。【参考方案2】:

尽管有文档,我假设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 肯定依赖于未定义的行为,但它应该可以正常工作。另一种方法是在我们的属性对象中使用三个函数(get、validate、set)而不是一个,但这违背了问题的重点。

但@tstanisl 指出它看起来像it isn't UB。太棒了!

【讨论】:

确定它是否是 UB。最近我发布了一个关于它的问题。见***.com/q/69771966/4989451。到目前为止,共识是定义resolve_offset @tstanisl,太棒了!我倾向于认为所有奇怪的东西都是 UB :) 有趣的方法。 tstanisl 的方法对我来说效果更好,特别是因为我的 Foo 结构中只有基本的 C 类型。 @M47,您的版本中有每个属性验证器。他们和我的唯一区别是我的有这些验证器,而他们的没有。

以上是关于在 PyGetSetDef 中使用闭包来重用属性 getter-setter的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift / Xcode 12.3 中重用代码块进行闭包

面试题总结

js闭包

如何创建一个可重用的函数来设置变量对象属性的状态?

为啥不对对象属性使用闭包?

访问在闭包内部定义的变量