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