As discussed in What is the function of the descriptors in Python? there is an order of calls that the interpreter executes when you g.attr
. As g
, in this case, is an instance, the interpreter will execute 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 look for the value of attr
in the class of g
, not g
directly. This explains why it works when the descriptor is a class attribute, but it is not enough to demonstrate that it does not work for instance attribute. For this, we will go deeper into the code and parse the C code that runs.
The C implementation of the __getattribute__
method is described by PyObject_GenericGetAttr which is implemented in Objects / object.c . Let's take a closer 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 receives as parameter obj
, a reference to the object g
;
The function receives as parameter name
, name of the attribute accessed;
The function receives as parameter dict
, a dictionary that, in this case, will be null;
From obj
look for the reference to its type, Grok
, by the variable tp
;
Initializes null pointers descr
, which will be a possible descriptor, res
, return of function, f
, function __get__
of the possible descriptor, as well as other pointers;
From this the name of the accessed attribute is validated, returning an error if the attribute is not a string . If it does, it increments 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);
Afterwards, the internal dictionary of type g
, tp
is validated, 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
is set to be the __get__
function of the value found in descr
. If you find the function and the descriptor is a data descriptor (it has the method __set__
), it defines res
as a result of __get__
and ends the function:
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 to see if the __set__
method exists on the object.
And it is so far that it runs when the (data) descriptor is a class attribute. For an instance attribute, execution continues. Now, since we're going to work directly with the instance, you'll have to consider your internal dictionary as well. So, the next step will be the union between the class and instance dictionaries, 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, it will be searched for by the attribute in the dict
dictionary and, if found, the value is returned:
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% with% will be the instance of the decorator, which will be returned as null, regardless of whether or not the PyDict_GetItem
defined.
If you can not find the attribute in the instance dictionary, it will be checked whether the descriptor found in the class is a non-data descriptor (which does not have the __get__
method) is called:
if (f != NULL) {
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
goto done;
}
After, if none of the above conditions have yet been met, it is checked if the __set__
object is other than null (found something about the attribute in descr
type), then g
is defined as the result returns it:
if (descr != NULL) {
res = descr;
descr = NULL;
goto done;
}
And finally, if nothing has worked out so far, it returns the attribute error not found:
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
Finally, move on the reference quantities and return the value of descr
:
done:
Py_XDECREF(descr);
Py_DECREF(name);
return res;
The entire function for better visualization 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;
}