max max - 2 months ago 10
Python Question

Differences in __dict__ between classes with and without an explicit base class

Why does a class that (implicitly) derives from

object
have more items in its
__dict__
attribute than a class that has an explicit base class? (python 3.5).

class X:
pass
class Y(X):
pass
X.__dict__
'''
mappingproxy({'__dict__': <attribute '__dict__' of 'X' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'X' objects>})
'''

Y.__dict__
'''
mappingproxy({'__doc__': None, '__module__': '__main__'})
'''

Answer

The definition of __weakref__ and __dict__ is so the appropriate descriptor gets invoked to access the "real" location of the weak reference list and the instance dictionary of instances of the class when Python level code looks for it. The base class object is bare bones, and does not reserve space for __weakref__ or __dict__ on instances (weakref.ref(object()) will fail, as will setattr(object(), 'foo', 0)).

For a user-defined class, it needs to define descriptors that can find those values (in CPython, these accessors are usually bypassed because there are direct pointers at the C layer, but if you explicitly reference instance.__dict__, it needs to know how to find it through the instance's class). The first child of object (without __slots__) needs to define these descriptors, so they can be found. The subclasses don't need to, because the location of __dict__ and __weakref__ doesn't change; they can continue to use the accessor for the base class, since their instances have those attributes at the same relative offset.

Basically, the first non-__slot__-ed user defined class is creating an idea of an instance as a C struct of the form:

struct InstanceObject {
    ... common object stuff ...
    ... anything defined by __slots__ in super class(es), if anything ...
    PyObject *location_of___dict__;
    PyObject *location_of___weakref__;
}

and the accessors are implemented to check the type to determine the offset of those location* struct members, then retrieve them. The struct definition of a child class doesn't change the offset of those location* members (if new __slots__ are added, they're just appended), so it can reuse the same accessor from the parent.