As commented on in What is the function of Python descriptors? there is a call order that the interpreter executes when you do g.attr
. Like g
, in this case, it is an instance, the interpreter will run g.__getattribute__('attr')
. In Python, what the interpreter will try to access is:
type(g).__dict__['attr'].__get__(g, type(g))
That is, it will seek the value of attr
in the class of g
, not in g
directly. This explains why it works when the descriptor is a class attribute, but it is not sufficient to demonstrate that it does not work for instance attribute. To do this, we will go deeper into the code and look at the C code that is executed.
The C implementation of the method __getattribute__
is described by Pyobject_genericgetattr which is implemented in Objects/Object. c. Let’s take a little look.
The function is:
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL);
}
And so we should look at the implementation of _PyObject_GenericGetAttrWithDict
.
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
{
PyTypeObject *tp = Py_TYPE(obj);
PyObject *descr = NULL;
PyObject *res = NULL;
descrgetfunc f;
Py_ssize_t dictoffset;
PyObject **dictptr;
...
}
Important information to continue:
- The function takes as a parameter
obj
, a reference to the object g
;
- The function takes as a parameter
name
, attribute name accessed;
- The function takes as a parameter
dict
, a dictionary which in this case shall be void;
- From
obj
sought the reference to its type, Grok
, by the variable tp
;
- Initializes null pointers
descr
, which will be a possible descriptor, res
, the return of function, f
, the function __get__
of the possible descriptor, as well as other pointers;
From this is validated the name of the accessed attribute, returning an error if the attribute is not a string. If it is, increment the number of references to the object with Py_INCREF
.
if (!PyUnicode_Check(name)){
PyErr_Format(PyExc_TypeError,
"attribute name must be string, not '%.200s'",
name->ob_type->tp_name);
return NULL;
}
Py_INCREF(name);
After, the internal dictionary of the type of g
, tp
, finalizing the function in case of failure:
if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < 0)
goto done;
}
After, it is searched for by the attribute in the class of g
, Grok
, saving in descr
. If found, the references are incremented and the value of f
as being the function __get__
of the value found in descr
. If you find the function and the descriptor is a data descriptor (it has the method __set__
), is defined res
as a result of __get__
and the function ends:
descr = _PyType_Lookup(tp, name);
f = NULL;
if (descr != NULL) {
Py_INCREF(descr);
f = descr->ob_type->tp_descr_get;
if (f != NULL && PyDescr_IsData(descr)) {
res = f(descr, obj, (PyObject *)obj->ob_type);
goto done;
}
}
The function that checks whether it is a data descriptor, PyDescr_IsData
, is defined by
#define PyDescr_IsData(d) (Py_TYPE(d)->tp_descr_set != NULL)
Which basically checks whether the method exists __set__
in the object.
And it is so far that it is executed when the (data) descriptor is a class attribute. For an instance attribute, execution continues. Now, as we will work directly with the instance, it will be necessary to also consider its internal dictionary. Thus, the next step will be the union between the dictionaries of the instance and the class, and the final pointer will be stored in dict
:
if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
dictoffset = tp->tp_dictoffset;
if (dictoffset != 0) {
if (dictoffset < 0) {
Py_ssize_t tsize;
size_t size;
tsize = ((PyVarObject *)obj)->ob_size;
if (tsize < 0)
tsize = -tsize;
size = _PyObject_VAR_SIZE(tp, tsize);
assert(size <= PY_SSIZE_T_MAX);
dictoffset += (Py_ssize_t)size;
assert(dictoffset > 0);
assert(dictoffset % SIZEOF_VOID_P == 0);
}
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
}
}
After that, you will be searched for the attribute in the dictionary dict
and, if found, is returned the value:
if (dict != NULL) {
Py_INCREF(dict);
res = PyDict_GetItem(dict, name);
if (res != NULL) {
Py_INCREF(res);
Py_DECREF(dict);
goto done;
}
Py_DECREF(dict);
}
Note that here, as the instance attribute will exist in the dictionary, the value returned in PyDict_GetItem
will be the instance of the decorator that, as will be different from null, will be returned, without considering whether there is, or not, the method __get__
defined.
If you do not find the attribute in the dictionary of the instance, it will be verified if the descriptor found in the class is a nondata (who does not have the method __set__
) and, if it exists, is called:
if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
goto done;
}
After, if it has not yet satisfied any of the above conditions, it is verified whether the descr
is different from null (found something about the attribute in the type of g
), then it is defined descr
as the result and the return:
if (descr != NULL) {
res = descr;
descr = NULL;
goto done;
}
And finally, if nothing has worked so far, returns the attribute error not found:
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
To conclude, we move the reference quantities and return the value of res
:
done:
Py_XDECREF(descr);
Py_DECREF(name);
return res;
The whole function, for better viewing is:
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, PyObject *dict)
{
PyTypeObject *tp = Py_TYPE(obj);
PyObject *descr = NULL;
PyObject *res = NULL;
descrgetfunc f;
Py_ssize_t dictoffset;
PyObject **dictptr;
if (!PyUnicode_Check(name)){
PyErr_Format(PyExc_TypeError,
"attribute name must be string, not '%.200s'",
name->ob_type->tp_name);
return NULL;
}
Py_INCREF(name);
if (tp->tp_dict == NULL) {
if (PyType_Ready(tp) < 0)
goto done;
}
descr = _PyType_Lookup(tp, name);
f = NULL;
if (descr != NULL) {
Py_INCREF(descr);
f = descr->ob_type->tp_descr_get;
if (f != NULL && PyDescr_IsData(descr)) {
res = f(descr, obj, (PyObject *)obj->ob_type);
goto done;
}
}
if (dict == NULL) {
/* Inline _PyObject_GetDictPtr */
dictoffset = tp->tp_dictoffset;
if (dictoffset != 0) {
if (dictoffset < 0) {
Py_ssize_t tsize;
size_t size;
tsize = ((PyVarObject *)obj)->ob_size;
if (tsize < 0)
tsize = -tsize;
size = _PyObject_VAR_SIZE(tp, tsize);
assert(size <= PY_SSIZE_T_MAX);
dictoffset += (Py_ssize_t)size;
assert(dictoffset > 0);
assert(dictoffset % SIZEOF_VOID_P == 0);
}
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
}
}
if (dict != NULL) {
Py_INCREF(dict);
res = PyDict_GetItem(dict, name);
if (res != NULL) {
Py_INCREF(res);
Py_DECREF(dict);
goto done;
}
Py_DECREF(dict);
}
if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
goto done;
}
if (descr != NULL) {
res = descr;
descr = NULL;
goto done;
}
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
done:
Py_XDECREF(descr);
Py_DECREF(name);
return res;
}
There are a few more things I need to comment on. I’ll see if I can do it tonight, if no one answers before.
– Woss
Thank you for the reply.
– ThiagoO