预计阅读本页时间:-
Instance Slots
By assigning a sequence of string attribute names to a special __slots__ class attribute, it is possible for a new-style class to both limit the set of legal attributes that instances of the class will have and optimize memory and speed performance.
This special attribute is typically set by assigning a sequence of string names to the variable __slots__ at the top level of a class statement: only those names in the __slots__ list can be assigned as instance attributes. However, like all names in Python, instance attribute names must still be assigned before they can be referenced, even if they’re listed in __slots__. For example:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
>>> class limiter(object):
... __slots__ = ['age', 'name', 'job']
...
>>> x = limiter()
>>> x.age # Must assign before use
AttributeError: age
>>> x.age = 40
>>> x.age
40
>>> x.ape = 1000 # Illegal: not in __slots__
AttributeError: 'limiter' object has no attribute 'ape'
Slots are something of a break with Python’s dynamic nature, which dictates that any name may be created by assignment. However, this feature is envisioned as both a way to catch “typo” errors like this (assignments to illegal attribute names not in __slots__ are detected), as well as an optimization mechanism. Allocating a namespace dictionary for every instance object can become expensive in terms of memory if many instances are created and only a few attributes are required. To save space and speed execution (to a degree that can vary per program), instead of allocating a dictionary for each instance, slot attributes are stored sequentially for quicker lookup.
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):
- If a subclass inherits from a superclass without a __slots__, the __dict__ attribute of the superclass will always be accessible, making a __slots__ in the subclass meaningless.
- If a class defines the same slot name as a superclass, the version of the name defined by the superclass slot will be accessible only by fetching its descriptor directly from the superclass.
- Because the meaning of a __slots__ declaration is limited to the class in which it appears, subclasses will have a __dict__ unless they also define a __slots__.
In terms of listing instance attributes generically, slots in multiple classes might require manual class tree climbs, dir usage, or a policy that treats slot names as a different category of names altogether:
>>> class E:
... __slots__ = ['c', 'd'] # Superclass has slots
...
>>> class D(E):
... __slots__ = ['a', '__dict__'] # So does its subclass
...
>>> X = D()
>>> X.a = 1; X.b = 2; X.c = 3 # The instance is the union
>>> X.a, X.c
(1, 3)
>>> E.__slots__ # But slots are not concatenated
['c', 'd']
>>> D.__slots__
['a', '__dict__']
>>> X.__slots__ # Instance inherits *lowest* __slots__
['a', '__dict__']
>>> X.__dict__ # And has its own an attr dict
{'b': 2}
>>> for attr in list(getattr(X, '__dict__', [])) + getattr(X, '__slots__', []):
... print(attr, '=>', getattr(X, attr))
...
b => 2 # Superclass slots missed!
a => 1
__dict__ => {'b': 2}
>>> dir(X) # dir() includes all slot names
[...many names omitted... 'a', 'b', 'c', 'd']
When such generality is possible, slots are probably best treated as class attributes, rather than trying to mold them to appear the same as normal instance attributes. For more on slots in general, see the Python standard manual set. Also watch for an example that allows for attributes based on both __slots__ and __dict__ storage in the Private decorator discussion of Chapter 38.
For a prime example of why generic programs may need to care about slots, see the lister.py display mix-in classes example in the multiple inheritance section of the prior chapter; a note there describes the example’s slot concerns. In such a tool that attempts to list attributes generically, slot usage requires either extra code or the implementation of policies regarding the handling of slot-based attributes in general.