Как создать слоты только для чтения?

По умолчанию слоты доступны для записи:

>>> class A: __slots__ = ('x',)
... 
>>> list(vars(A))
['__module__', '__slots__', 'x', '__doc__']
>>> vars(A)['x']
<member 'x' of 'A' objects>
>>> a = A()
>>> a.x = 'foo'
>>> del a.x

Как создать слоты только для чтения, например слоты '__thisclass__' , '__self__' и '__self_class__' класса super?

>>> list(vars(super))
['__repr__', '__getattribute__', '__get__', '__init__', '__new__',
 '__thisclass__', '__self__', '__self_class__', '__doc__']
>>> vars(super)['__thisclass__']
<member '__thisclass__' of 'super' objects>
>>> s = super(int, 123)
>>> s.__thisclass__ = float
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: readonly attribute
>>> del s.__thisclass__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: readonly attribute

person Maggyero    schedule 08.04.2021    source источник


Ответы (1)


Вы не можете, в коде Python нет возможности создавать дескрипторы только для чтения, подобные тем, которые используются для __thisclass__ и т. д.

В C API все слоты используют один и тот же тип объекта дескриптора, который становится доступным только для чтения для записей в PyMemberDef массивы, у которых flags установлено в READONLY.

Например. идентифицированные вами дескрипторы super определены в массиве super_members:

static PyMemberDef super_members[] = {
    {"__thisclass__", T_OBJECT, offsetof(superobject, type), READONLY,
     "the class invoking super()"},
    {"__self__",  T_OBJECT, offsetof(superobject, obj), READONLY,
     "the instance invoking super(); may be None"},
    {"__self_class__", T_OBJECT, offsetof(superobject, obj_type), READONLY,
     "the type of the instance invoking super(); may be None"},
    {0}
};

Когда __slots__ обрабатывается, нет пути, по которому вы можете установить этот флаг; код в type.__new__ просто устанавливает первые три значения, то есть name, type и offset соответственно:

    mp = PyHeapType_GET_MEMBERS(et);
    slotoffset = base->tp_basicsize;
    if (et->ht_slots != NULL) {
        for (i = 0; i < nslots; i++, mp++) {
            mp->name = PyUnicode_AsUTF8(
                PyTuple_GET_ITEM(et->ht_slots, i));
            if (mp->name == NULL)
                goto error;
            mp->type = T_OBJECT_EX;
            mp->offset = slotoffset;


            /* __dict__ and __weakref__ are already filtered out */
            assert(strcmp(mp->name, "__dict__") != 0);
            assert(strcmp(mp->name, "__weakref__") != 0);


            slotoffset += sizeof(PyObject *);
        }
    }

Для справки:

  • PyHeapType_GET_MEMBERS обращается к массиву PyMemberDef для создаваемого объекта нового типа. Для него уже выделено нужное количество слотов.
  • et->ht_slots — это кортеж имен слотов.
  • slotoffset — это относительное смещение в области памяти объекта экземпляра для хранения содержимого слота.
  • значение T_OBJECT_EX для поля type означает, что слот хранит указатель на объект Python, и если указатель установлен на NULL, при попытке получить значение поднимается AttributeError.

Обратите внимание: если бы была возможность сделать эти слоты доступными только для чтения, вам также понадобился бы механизм предоставления их значений перед созданием нового экземпляра! В конце концов, если всему коду Python запретить установку атрибута «только для чтения» для экземпляров, как ваш класс Python вообще сможет установить начальное значение?

person Martijn Pieters    schedule 09.04.2021