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