1

According to the Python language documentation:

__slots__ allow us to explicitly declare data members (like properties) and deny the creation of __dict__ and __weakref__ (unless explicitly declared in __slots__ or available in a parent.)

So I am wondering if this class

>>> class A: __slots__ = ('__dict__', '__weakref__')
... 
>>> del A.__slots__
>>> vars(A)
mappingproxy({
    '__module__': '__main__',
    '__dict__': <attribute '__dict__' of 'A' objects>,
    '__weakref__': <attribute '__weakref__' of 'A' objects>,
    '__doc__': None
})

is equivalent to this one:

>>> class A: pass
... 
>>> vars(A)
mappingproxy({
    '__module__': '__main__',
    '__dict__': <attribute '__dict__' of 'A' objects>,
    '__weakref__': <attribute '__weakref__' of 'A' objects>,
    '__doc__': None
})
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Géry Ogam
  • 6,336
  • 4
  • 38
  • 67

1 Answers1

4

If you don’t set a __slots__ attribute, the default is to create the __weakref__ and __dict__ slots in certain conditions.

This happens in type.__new__(); a class is given descriptors to access memory slots for each instance:

  • First, a check is made if the type can have these slots by testing for the tp_dictoffset, tp_weaklistoffset and tp_itemsize values in the type object struct.

    • You can only have a __dict__ slot if the base class doesn’t already define one (base->tp_dictoffset is 0).
    • You can only have a __weakref__ slot if the base type doesn’t define one (base->tp_weaklistoffset is 0) and the base has fixed-length instances (base->tp_itemsize is 0).
  • Then, if there is no __slots__, flag variables are set to actually enable the slots being added.

The type.__new__() implementation is long, but the relevant section looks like this:

    slots = _PyDict_GetItemIdWithError(dict, &PyId___slots__);
    nslots = 0;
    add_dict = 0;
    add_weak = 0;
    may_add_dict = base->tp_dictoffset == 0;
    may_add_weak = base->tp_weaklistoffset == 0 && base->tp_itemsize == 0;
    if (slots == NULL) {
        if (PyErr_Occurred()) {
            goto error;
        }
        if (may_add_dict) {
            add_dict++;
        }
        if (may_add_weak) {
            add_weak++;
        }
    }
    else {
        /* Have slots */
        ...
    }

Then, some 400 lines lower down, you'll find the code that actually creates the slots:

    if (add_dict) {
        if (base->tp_itemsize)
            type->tp_dictoffset = -(long)sizeof(PyObject *);
        else
            type->tp_dictoffset = slotoffset;
        slotoffset += sizeof(PyObject *);
    }
    if (add_weak) {
        assert(!base->tp_itemsize);
        type->tp_weaklistoffset = slotoffset;
        slotoffset += sizeof(PyObject *);
    }

When extending certain native types (such as int or bytes) you don’t get a __weakref__ slot as they have variable-length instances (handled by tp_itemsize).

Géry Ogam
  • 6,336
  • 4
  • 38
  • 67
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Thanks a lot. “For Python classes, this is always the case” Except for subclasses of a *variable-length* type (like the built-in types `int`, `tuple`, and `bytes`) since they have a non zero `__itemsize__` attribute and therefore they will not get a `__weakref__` attribute, right? – Géry Ogam Apr 09 '21 at 20:57
  • @Maggyero right, it depends on the type of the base. I should have been clearer. – Martijn Pieters Apr 09 '21 at 20:59
  • Nice edit. About the terminology, when we say “slots”, are we talking about the descriptors on the class or the reserved storage on each class instance referenced by the descriptors, or both? – Géry Ogam Apr 09 '21 at 21:11
  • @Maggyero the reserved storage. The descriptors are the bridge between that memory and Python code. – Martijn Pieters Apr 09 '21 at 21:19
  • The [Descriptor HowTo](https://docs.python.org/3/howto/descriptor.html#member-objects-and-slots) seems to confirm that slots are the reserved storage on class *instances* like you said: “When a class defines `__slots__`, it replaces instance dictionaries with a fixed-length array of slot values.” and “The `object.__new__()` method takes care of creating instances that have slots instead of an instance dictionary.” – Géry Ogam Apr 10 '21 at 11:14
  • Yet in your answer you said: “remember, these slots live on the class object, they are descriptors” and “you'll find the code that actually creates the slots” and `type->tp_dictoffset = slotoffset;` and `type->tp_weaklistoffset = slotoffset;`. This seems to contradict that slots live on the class *instance* since, as I understand it, the code you are referring to creates a class with the relevant data descriptors (`__dict__`, `__weakref__`, or members listed in `__slots__`), not a class instance with the relevant reserved slots. Am I misunderstanding something? – Géry Ogam Apr 10 '21 at 11:32
  • @Maggyero: sorry, that can indeed be confusing. The class structure has to track what the instance structure will look like, so the class tracks the offsets. – Martijn Pieters Apr 11 '21 at 13:14