How to create read-only slots?
Asked Answered
A

1

3

Slots are writable by default:

>>> 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

How to create read-only slots, like the slots '__thisclass__' , '__self__', and '__self_class__' of the 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
Arguelles answered 8/4, 2021 at 17:55 Comment(0)
P
6

You can't, there is no option from Python code to create read-only descriptors like the ones used for __thisclass__, etc.

In the C API, slots all use the same descriptor object type, which become read-only for entries in PyMemberDef arrays that have flags set to READONLY.

E.g. the super descriptors you identified are defined in the super_members array:

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}
};

When __slots__ is being processed, there is no path where you can set this flag; the code in type.__new__ just sets the first three values, which are name, type and offset, respectively:

    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 *);
        }
    }

For reference:

  • PyHeapType_GET_MEMBERS accesses the PyMemberDef array for the new type object being created. It has the right number of slots allocated already.
  • et->ht_slots is the tuple of slot names.
  • slotoffset is the relative offset into the instance object memory area to store the slot contents.
  • the T_OBJECT_EX value for the type field means the slot stores a pointer to a Python object, and if the pointer is set to NULL an AttributeError is raised when you try to get the value.

Note that if it was possible to set these slots to read-only, you'd also need a mechanism to provide their value before creating a new instance! After all, if all Python code was prevented from setting a readonly attribute on instances, how would your Python class ever get to set the initial value?

Publicize answered 9/4, 2021 at 18:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.