The Python Language Reference, Release 3.9.5 Invoking Descriptors In general, a descriptor is an object attribute with “binding behavior”, one whose attribute access has been overridden by methods in the descriptor protocol:$_{__get__()}$ ,$_{__set__()}$ , and$_{__delete__()}$ . If any of those methods are defined for an object, it is said to be a descriptor. The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting with a.__dict__['x'] , then type(a).__dict__['x'] , and continuing through the base classes of type(a) excluding metaclasses. However, if the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined and how they were called. The starting point for descriptor invocation is a binding,$_{a.x}$ . How the arguments are assembled depends on$_{a}$: Direct Call The simplest and least common call is when user code directly invokes a descriptor method: x. __get__(a) . Instance Binding If binding to an object instance, a.x is transformed into the call: type(a). __dict__['x'].__get__(a, type(a)) . Class Binding If binding to a class, A.x is transformed into the call: A.__dict__['x'].__get__(None, $_{A)}$. Super Binding If a is an instance of $_{super}$, then the binding super(B, obj).m() searches obj. __class__.__mro__ for the base class$_{A}$ immediately preceding$_{B}$ and then invokes the descriptor with the call:$_{A.__dict__['m'].__get__(obj, obj.__class__)}$ . For instance bindings, the precedence of descriptor invocation depends on which descriptor methods are defined. A descriptor can define any combination of$_{__get__()}$ ,$_{__set__()}$ and$_{__delete__()}$ . If it does not define$_{__get__()}$ , then accessing the attribute will return the descriptor object itself unless there is a value in the object’s instance dictionary. If the descriptor defines$_{__set__()}$ and/or$_{__delete__()}$ , it is a data de- scriptor; if it defines neither, it is a non-data descriptor. Normally, data descriptors define both$_{__get__()}$ and __set__() , while non-data descriptors have just the$_{__get__()}$ method. Data descriptors with$_{__get__()}$ and$_{__set__()}$ (and/or$_{__delete__()}$ ) defined always override a redefinition in an instance dictionary. In contrast, non-data descriptors can be overridden by instances. Python methods (including$_{staticmethod()}$ and$_{classmethod()}$ ) are implemented as non-data descriptors. Accordingly, instances can redefine and override methods. This allows individual instances to acquire behaviors that differ from other instances of the same class. The$_{property()}$ function is implemented as a data descriptor. Accordingly, instances cannot override the behavior of a property. __slots__ allow us to explicitly declare data members (like properties) and deny the creation of __dict__ and __weakref__ (unless explicitly declared in __slots__ or available in a parent.) The space saved over using __dict__ can be significant. Attribute lookup speed can be significantly improved as well. object. __slots__ This class variable can be assigned a string, iterable, or sequence of strings with variable names used by in- stances. __slots__ reserves space for the declared variables and prevents the automatic creation of __dict__ and __weakref__ for each instance. __slots__ Chapter 3. Data model 32