Attributes and Subclasses

Namespaces in Classes

In much the same way that modules and packages create namespaces and hold objects inside for use in scripts, classes hold attributes within each class definition for use in instances.

Motivation

By using the the ‘__dict__’ hidden method, we can interrogate the attributes of a class.

Vanilla case

class Klass(object):
    def __init__(self):
        self.a = 1
    
ex = Klass()
ex.__dict__
{'a': 1}

Nested class

Now if we make a class that inherits from our last class.

class SubKlass(Klass):
    pass

ex = SubKlass()
ex.__dict__
{'a': 1}

We still get the same dict.

The Catch

It’s poor style, but we can add attributes to a class object without going through the typical __init__ protocol.

Klass.b = 2
Klass.c = 3

ex = Klass()

However, when investigating the instance’s __dict__ method for attributes, b and c are nowhere to be found.

ex.__dict__
{'a': 1}

Spookily, though. We can still use them.

print(ex.b, ex.c)
2 3

If we investigate the contents of our intance with the dir function, we see that b and c are readliy available.

list(x for x in dir(ex) if not x.startswith('__'))
['a', 'b', 'c']

__dict__: instance vs class

To bring this example home, let’s see what happens when we call __dict__ on a class vs an instance.

Klass.__dict__
mappingproxy({'__dict__': <attribute '__dict__' of 'Klass' objects>,
              '__doc__': None,
              '__init__': <function __main__.Klass.__init__>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'Klass' objects>,
              'b': 2,
              'c': 3})
ex.__dict__
{'a': 1}

Concretely, the class has the b and c attributes available and the instance knows how to get to them. This is achieved under the hood by a recursive scan through everything that “makes” the instance.

To investigate this behavior, we can check the built-in __class__ attribute to see what class an instance was created with.

ex.__class__
__main__.Klass

We can then chain that operation to see what the __dict__ looks like for that class (which is essentially what we did at the beginning of this header).

ex.__class__.__dict__
mappingproxy({'__dict__': <attribute '__dict__' of 'Klass' objects>,
              '__doc__': None,
              '__init__': <function __main__.Klass.__init__>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'Klass' objects>,
              'b': 2,
              'c': 3})

Notice that we’re looking at this fancy-pants mappingproxy object. This is essentially a representation of the underlying __dict__, but that we can’t explicitly make changes to. It’s all derived from whatever lives in the class through assignment.

But to summarize:

When an object is instantiated from a class, it has access to every attribute all the way up the inheritence chain. However, it can be difficult to know what’s available if you’re not explicitly assigning things in the proper __init__ format.

So use __init__.