Learning Python - Mark Lutz [429]
Slots and generic code
In fact, some instances with slots may not have a __dict__ attribute dictionary at all, which can make some metaprograms more complex (including some coded in this book). Tools that generically list attributes or access attributes by string name, for example, must be careful to use more storage-neutral tools than __dict__, such as the getattr, setattr, and dir built-in functions, which apply to attributes based on either __dict__ or __slots__ storage. In some cases, both attribute sources may need to be queried for completeness.
For example, when slots are used, instances do not normally have an attribute dictionary—Python uses the class descriptors feature covered in Chapter 37 to allocate space for slot attributes in the instance instead. Only names in the slots list can be assigned to instances, but slot-based attributes can still be fetched and set by name using generic tools. In Python 3.0 (and in 2.6 for classes derived from object):
>>> class C:
... __slots__ = ['a', 'b'] # __slots__ means no __dict__ by default
...
>>> X = C()
>>> X.a = 1
>>> X.a
1
>>> X.__dict__
AttributeError: 'C' object has no attribute '__dict__'
>>> getattr(X, 'a')
1
>>> setattr(X, 'b', 2) # But getattr() and setattr() still work
>>> X.b
2
>>> 'a' in dir(X) # And dir() finds slot attributes too
True
>>> 'b' in dir(X)
True
Without an attribute namespaces dictionary, it’s not possible to assign new names to instances that are not names in the slots list:
>>> class D:
... __slots__ = ['a', 'b']
... def __init__(self): self.d = 4 # Cannot add new names if no __dict__
...
>>> X = D()
AttributeError: 'D' object has no attribute 'd'
However, extra attributes can still be accommodated by including __dict__ in __slots__, in order to allow for an attribute namespace dictionary. In this case, both storage mechanisms are used, but generic tools such as getattr allow us to treat them as a single set of attributes:
>>> class D:
... __slots__ = ['a', 'b', '__dict__'] # List __dict__ to include one too
... c = 3 # Class attrs work normally
... def __init__(self): self.d = 4 # d put in __dict__, a in __slots__
...
>>> X = D()
>>> X.d
4
>>> X.__dict__ # Some objects have both __dict__ and __slots__
{'d': 4} # getattr() can fetch either type of attr
>>> X.__slots__
['a', 'b', '__dict__']
>>> X.c
3
>>> X.a # All instance attrs undefined until assigned
AttributeError: a
>>> X.a = 1
>>> getattr(X, 'a',), getattr(X, 'c'), getattr(X, 'd')
(1, 3, 4)
Code that wishes to list all instance attributes generically, though, may still need to allow for both storage forms, since dir also returns inherited attributes (this relies on dictionary iterators to collect keys):
>>> for attr in list(X.__dict__) + X.__slots__:
... print(attr, '=>', getattr(X, attr))
d => 4
a => 1
b => 2
__dict__ => {'d': 4}
Since either can be omitted, this is more correctly coded as follows (getattr allows for defaults):
>>> for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
... print(attr, '=>', getattr(X, attr))
d => 4
a => 1
b => 2
__dict__ => {'d': 4}
Multiple __slot__ lists in superclasses
Note, however, that this code addresses only slot names in the lowest __slots__ attribute inherited by an instance. If multiple classes in a class tree have their own __slots__ attributes, generic programs must develop other policies for listing attributes (e.g., classifying slot names as attributes of classes, not instances).
Slot declarations can appear in multiple classes in a class tree, but they are subject to a number of constraints that are somewhat difficult to rationalize unless you understand the implementation of slots as class-level descriptors (a tool we’ll study in detail in the last part of this book):