ООП. Разрешение имен атрибтутов
Мы уже рассмотрели как происходит поиск символьного имени («имени переменной») в лекции посвященной пространству имен и областям видимости. В этой лекции мы поговорим о разрешении имен атрибутов и от том как можно «вмешаться» в поиск атрибута переопределив магические методы __getattr__
и __getattribute__
.
Давайте рассмотрим следующий пример, пусть у нас есть класс Goofy
и мы обращаемся к атрибуту x
у экзмепляра этого класса:
source = '''
class Goofy:
pass
g = Goofy()
g.x
'''
Для вас не должно стать сюрпризом, что выполнение этого кода завершится исключением AttributeError
. Но как CPython определил, что атрибута x
не существует и следует возбудить исключение AttributeError
? Давайте начнем с того, что определим какая инструкция в байт-коде отвечает за поиск атрибута:
import dis
dis.dis(source)
6 20 LOAD_NAME 1 (g)
22 LOAD_ATTR 2 (x)
24 POP_TOP
26 LOAD_CONST 2 (None)
28 RETURN_VALUE
Инструкция LOAD_NAME
вам уже должна быть знакома, она помещает значение g
на стек. Нас интересует инструкция LOAD_ATTR
:
case TARGET(LOAD_ATTR): {
PyObject *name = GETITEM(names, oparg);
PyObject *owner = TOP();
PyObject *res = PyObject_GetAttr(owner, name);
Py_DECREF(owner);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}
Макрос GETITEM
возвращает указатель на искомый атрибут (x
), а TOP
возвращает указатель на объект, который находится на вершине стека (g
), на нем и осуществляется поиск. Затем происходит вызов функции PyObject_GetAttr
, которая определена в Objects/objects.c:
PyObject *
PyObject_GetAttr(PyObject *v, PyObject *name)
{
PyTypeObject *tp = Py_TYPE(v);
if (!PyUnicode_Check(name)) {
PyErr_Format(PyExc_TypeError,
"attribute name must be string, not '%.200s'",
name->ob_type->tp_name);
return NULL;
}
if (tp->tp_getattro != NULL)
return (*tp->tp_getattro)(v, name);
if (tp->tp_getattr != NULL) {
const char *name_str = PyUnicode_AsUTF8(name);
if (name_str == NULL)
return NULL;
return (*tp->tp_getattr)(v, (char *)name_str);
}
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
return NULL;
}
Во-первых, мы получаем ссылку на тип объекта (в данном случае Goofy
), который фактически определяет допустимые операции над объектом. Затем осуществляется проверка какой из методов доступен: tp_getattro
или tp_getattr
, последний является устаревшим и не рекомендуется к использованию. Оба метода, или как их называют - слота, содержат указатель на одну из трех возможных функций: PyObject_GenericGetAttr
, slot_tp_getattro
или slot_tp_getattr_hook
, одна из которых и будет вызвана для выполнения поиска атрибута. Остается выяснить какая именно функция.
Решение о том какую именно функцию следует установить в слот tp_getattro
происходит во время создания типа (в нашем случае класса Goofy
), а именно в вызове функции type_new
. В конце вызова type_new
происходит вызов диспетчера слотов fixup_slot_dispatcher
, который отвечает за инициализацию слотов с учетом переопределенных пользователем магических методов, другими словами, будет выбрана или пользовательская реализация (specific) или реализация по умолчанию (generic). Итак, если пользователь переопределил __getattr__
или __getattribute__
, то будет установлен slot_tp_getattr_hook
, иначе PyObject_GenericGetAttr
.
Note
Более подробно о создании типов и функции tp_new
мы будем говорить в лекции посвященной метаклассам.
Давайте начнем с рассмотрения принципов работы функции PyObject_GenericGetAttr
, которая чаще всего вызывается при осуществлении поиска атрибутов:
PyObject *
PyObject_GenericGetAttr(PyObject *obj, PyObject *name)
{
return _PyObject_GenericGetAttrWithDict(obj, name, NULL, 0);
}
PyObject *
_PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name,
PyObject *dict, int suppress)
{
PyTypeObject *tp = Py_TYPE(obj);
...
descr = _PyType_Lookup(tp, name); // (1)
f = NULL;
if (descr != NULL) {
Py_INCREF(descr);
f = descr->ob_type->tp_descr_get;
if (f != NULL && PyDescr_IsData(descr)) { // (2)
res = f(descr, obj, (PyObject *)obj->ob_type);
...
goto done;
}
}
if (dict == NULL) { // (3)
/* Inline _PyObject_GetDictPtr */
dictoffset = tp->tp_dictoffset;
if (dictoffset != 0) {
...
dictptr = (PyObject **) ((char *)obj + dictoffset);
dict = *dictptr;
}
}
if (dict != NULL) { // (4)
Py_INCREF(dict);
res = PyDict_GetItemWithError(dict, name);
if (res != NULL) {
...
goto done;
}
...
}
if (f != NULL) { // (5)
res = f(descr, obj, (PyObject *)Py_TYPE(obj));
...
goto done;
}
if (descr != NULL) { // (6)
res = descr;
descr = NULL;
goto done;
}
if (!suppress) {
PyErr_Format(PyExc_AttributeError,
"'%.50s' object has no attribute '%U'",
tp->tp_name, name);
}
done:
...
return res;
}
- Поиск атрибута осуществляется сначала в словаре класса, а затем в родительских классах в порядке определяемым MRO.
- Если атрибут был найден, то проверяется является ли найденный атрибут дескриптором данных (data descriptior, дескриптор у которого определены методы
__get__
и__set__
) и если является, то происходит вызов метода__get__
у дескриптора. - Получение смещения для словаря экземпляра класса, а затем по смещению и самого словаря (
f.__dict__
). - Если атрибут не был найден в цепочке MRO или не является дескпритором данных, то поиск осуществляется в словаре экзмепляра класса.
- Если в словаре экземпляра атрибут не найден, но ранее был найден в цепочке MRO, то проверяется является ли найденный атрибут дескриптором не данных (non-data descriptor, дескриптор у которого определен только метод
__get__
) и если является, то вызывается метод__get__
у дескриптора. - Если атрибут не является дескриптором не данных, но был найден в цепочке MRO, то возвращается найденное значение.
- Атрибут не найден, порождается исключение
AttributeError
.
Note
MRO (Method Resolution Order, порядок разрешения методов) определяет порядок поиска атрибутов и методов в классах предках, если они не обнаружены непосредственно в классе-потомке. Более подробно про MRO мы будем говорить в лекции посвященной наследованию.
Теперь перейдем к функции slot_tp_getattr_hook
:
static PyObject *
slot_tp_getattro(PyObject *self, PyObject *name)
{
PyObject *stack[1] = {name};
return call_method(self, &PyId___getattribute__, stack, 1);
}
static PyObject *
slot_tp_getattr_hook(PyObject *self, PyObject *name)
{
PyTypeObject *tp = Py_TYPE(self);
...
getattr = _PyType_LookupId(tp, &PyId___getattr__);
if (getattr == NULL) {
tp->tp_getattro = slot_tp_getattro;
return slot_tp_getattro(self, name);
}
...
getattribute = _PyType_LookupId(tp, &PyId___getattribute__);
if (getattribute == NULL ||
(Py_TYPE(getattribute) == &PyWrapperDescr_Type &&
((PyWrapperDescrObject *)getattribute)->d_wrapped ==
(void *)PyObject_GenericGetAttr))
res = PyObject_GenericGetAttr(self, name);
else {
Py_INCREF(getattribute);
res = call_attribute(self, getattribute, name);
Py_DECREF(getattribute);
}
if (res == NULL && PyErr_ExceptionMatches(PyExc_AttributeError)) {
PyErr_Clear();
res = call_attribute(self, getattr, name);
}
Py_DECREF(getattr);
return res;
}
Задачей функции slot_tp_getattr_hook
является определить какой из магических методов __getattr__
или __getattribute__
был переопределен пользователем. Сначала проверяется был ли переопределен метод __getattr__
, если нет, то это означает, что переопределен метод __getattribute__
и слот подменяется на функцию slot_tp_getattro
, которая просто вызывает пользовательский метод. В противном случае интерпретатор проверяет был ли переопределен метод __getattribute__
, так как вызов этого метода происходит безусловно. Здесь следует иметь ввиду, что все пользовательские типы имеют своим предком PyBaseObject_Type
(object
), у которого в слот tp_getattro
установлена функция PyObject_GenericGetAttr
, которая и будет реализацией по умолчанию для метода __getattribute__
. Если в результате вызова __getattribute__
атрибут не был найден и было порождено исключение AttributeError
, то будет вызван переопределенный метод __getattr__
.