9

What is the best way to create class properties (as here and here) using the Python C-API? Static properties would also work in my case.

Follow up:

I tried to implement yak's suggestion. I defined a class P with get and set functions in its tp_descr_get and tp_descr_set slots. Then I added an instance of P to the dictionary of the type object for the class X under the key p. Then

x1 = X()
x2 = X()
x1.p = 10
print x1.p
print x2.p
print X.p
x2.p = 11
print x1.p
print x2.p
print X.p

works (first 10 is printed three times, then 11 is printed three times), but

X.p = 12

fails with the error message

TypeError: can't set attributes of built-in/extension type 'X'

How do I fix that?

Follow up 2:

If I allocate the type object with PyMem_Malloc and set the Py_TPFLAGS_HEAPTYPE flag, then everything works ; I can do X.p = 12 with the expected result.

Things also work if I keep the type object in a static variable and set the Py_TPFLAGS_HEAPTYPE flag, but that is obviously not a good idea. (But why does it matter whether the type object is in static or dynamic memory? I never let its reference count drop to 0 anyway.)

The restriction that you can only set attributes on dynamic types seems very strange. What is the rationale behind this?

Follow up 3:

No, it does not work. If I make the type X dynamic, then X.p = 12 does not set the property X.p to twelve; it actually binds the object 12 to the name X.p. In other words, afterwards, X.p is not an integer-valued property but an integer.

Follow up 4:

Here is the C++ code for the extension:

#include <python.h>
#include <exception>

class ErrorAlreadySet : public std::exception {};

// P type ------------------------------------------------------------------

struct P : PyObject
{
    PyObject* value;
};

PyObject* P_get(P* self, PyObject* /*obj*/, PyObject* /*type*/)
{
    Py_XINCREF(self->value);
    return self->value;
}

int P_set(P* self, PyObject* /*obj*/, PyObject* value)
{
    Py_XDECREF(self->value);
    self->value = value;
    Py_XINCREF(self->value);
    return 0;
}

struct P_Type : PyTypeObject
{
    P_Type()
    {
        memset(this, 0, sizeof(*this));
        ob_refcnt = 1;
        tp_name = "P";
        tp_basicsize = sizeof(P);
        tp_descr_get = (descrgetfunc)P_get;
        tp_descr_set = (descrsetfunc)P_set;
        tp_flags = Py_TPFLAGS_DEFAULT;

        if(PyType_Ready(this)) throw ErrorAlreadySet();
    }
};

PyTypeObject* P_type()
{
    static P_Type typeObj;
    return &typeObj;
}


// P singleton instance ----------------------------------------------------

P* createP()
{
    P* p_ = PyObject_New(P, P_type());
    p_->value = Py_None;
    Py_INCREF(p_->value);
    return p_;
}

P* p()
{
    static P* p_ = createP();
    Py_INCREF(p_);
    return p_;
}

PyObject* p_value()
{
    PyObject* p_ = p();
    PyObject* value = p()->value;
    Py_DECREF(p_);
    Py_INCREF(value);
    return value;
}


// X type ------------------------------------------------------------------

struct X : PyObject {};

void X_dealloc(PyObject* self)
{
    self->ob_type->tp_free(self);
}

struct X_Type : PyTypeObject
{
    X_Type()
    {
        memset(this, 0, sizeof(*this));
        ob_refcnt = 1;
        tp_name = "M.X";
        tp_basicsize = sizeof(X);
        tp_dealloc = (destructor)X_dealloc;
        tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HEAPTYPE;

        tp_dict = PyDict_New();
        PyObject* key = PyString_FromString("p");
        PyObject* value = p();
        PyDict_SetItem(tp_dict, key, value);
        Py_DECREF(key);
        Py_DECREF(value);

        if(PyType_Ready(this)) throw ErrorAlreadySet();
    }

    void* operator new(size_t n) { return PyMem_Malloc(n); }
    void operator delete(void* p) { PyMem_Free(p); }
};

PyTypeObject* X_type()
{
    static PyTypeObject* typeObj = new X_Type;
    return typeObj;
}

// module M ----------------------------------------------------------------

PyMethodDef methods[] = 
{
    {"p_value", (PyCFunction)p_value, METH_NOARGS, 0},
    {0, 0, 0, 0}
};

PyMODINIT_FUNC
initM(void)
{
    try {
        PyObject* m = Py_InitModule3("M", methods, 0);
        if(!m) return;
        PyModule_AddObject(m, "X", (PyObject*)X_type());
    }
    catch(const ErrorAlreadySet&) {}
}

This code defines a module M with a class X with a class property p as described before. I have also added a function p_value() that lets you directly inspect the object that implements the property.

Here is the script I have used to test the extension:

from M import X, p_value

x1 = X()
x2 = X()

x1.p = 1
print x1.p
print x2.p
print X.p
print p_value()
print

x2.p = 2
print x1.p
print x2.p
print X.p
print p_value()
print

X.p = 3
print x1.p
print x2.p
print X.p
print p_value()     # prints 2
print

x1.p = 4       # AttributeError: 'M.X' object attribute 'p' is read-only
1

4 Answers 4

6

Similar to these Python solutions, you will have to create a classproperty type in C and implement its tp_descr_get function (which corresponds to __get__ in Python).

Then, if you want to use that in a C type, you would have to create an instance of your classproperty type and insert it into dictionary of your type (tp_dict slot of your type).

Follow up:

It would seem that it's impossible to set an attribute of a C type. The tp_setattro function of the metaclass (PyType_Type) raises a "can't set attributes of built-in/extension type" exception for all non-heap types (types with no Py_TPFLAGS_HEAPTYPE flag). This flag is set for dynamic types. You could make your type dynamic but it might be more work then it's worth.

This means that the solution I gave initially allows you to create a property (as in: computed attribute) on a C type object with the limitation that it's read only. For setting you could use a class/static-method (METH_CLASS/METH_STATIC flag on a method in tp_methods).

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks! I tried it and it almost works; see the edited question.
Thanks! I tried using dynamic types and it works; see the edited question.
Can you post what you have so far?
Turns out that tp_descr_set isn't called when a type attribute is set, it works only on instances. A workaround is to move the descriptor to metatype. Your type will be an instance of the metatype so descriptor will work. You would set the metatype as ob_type of X. All this applies to Python as well, see this (python.6.n6.nabble.com/…) for example.
If getting/setting p on an instance of X rather than on the type itself is an option, then things become much simpler because you can use tp_getset.
|
4

I'll try to convey the essence of what I've discovered about using class static properties.

My (edited) code is as follows:

// Prepare your type object, which you will no doubt complete 
// by calling PyType_Ready, as here.
if (PyType_Ready(typeObj) < 0)
{
  return;
}

Py_INCREF(typeObj);
PyModule_AddObject(module, typeName, (PyObject*) typeObj);

// Now we add static members directly to the type's tp_dict, but 
// only *after* we've registered the type (above)
PyObject* dict = typeObj->tp_dict;

// You'll have your own wrapper / C values in the line below. This is just
// a pseudo-version of what I am (successfully) using.
PyObject* tmp = MyCreateWrapper(myStaticValueInC);

// Py_INCREF(tmp); // You may need this, depending on how line above works.

PyDict_SetItemString(dict, staticPropertyName, tmp);

Py_DECREF(tmp);

I believe all the essentials are here in terms of what order to construct your code in order to implement a class property.

1 Comment

Modifying tp_dict in this way appears to be prohibited: "Warning It is not safe to use PyDict_SetItem() on or otherwise modify tp_dict with the dictionary C-API."
1

If that is an acceptable solution, you may create a method on the module holding the declaration of X, that simply sets the class variable as you wish. For example:

PyObject* set_p_value(PyObject*, PyObject* o) {
  if(PyDict_SetItemString(X_type()->tp_dict, "p", o) == -1) return 0;
  Py_RETURN_NONE; 
}

PyMethodDef methods[] = 
{
    ...
    {"set_p_value", (PyCFunction)set_p_value, METH_O, 0},
    {0, 0, 0, 0}
};

Once that is there, than:

from M import X, set_p_value
set_p_value(3)
print X.p #should print '3'

Should work as expected. A draw-back is that, unfortunately, this functionality is unrelated to the type object itself. This could be partially circumvented if you provided a class method that sets the class variable as you wish.

1 Comment

Modifying tp_dict in this way appears to be prohibited: "Warning It is not safe to use PyDict_SetItem() on or otherwise modify tp_dict with the dictionary C-API."
0

You can add items to tp_dict as long as it happens before you call PyType_Ready:

int err;
PyObject *p;

if (!X_Type.tp_dict) {
    X_Type.tp_dict = PyDict_New();
    if (!X_Type.tp_dict)
        return NULL;
}

p = PyLong_FromLong(12);
if (!p)
    return NULL;

err = PyDict_SetItemString(X_Type.tp_dict, "p", p)
Py_DECREF(p);
if (err)
    return NULL;

m = PyModule_Create(&M_module);
if (!m)
    return NULL;

if (PyModule_AddType(m, &X_type));
    return NULL;

return m;

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.