- Mastering Objectoriented Python
- Steven F. Lott
- 928字
- 2021-11-12 16:25:14
Creating descriptors
A descriptor is a class that mediates attribute access. The descriptor class can be used to get, set, or delete attribute values. Descriptor objects are built inside a class at class definition time.
The descriptor design pattern has two parts: an owner class and the attribute descriptor itself. The owner class uses one or more descriptors for its attributes. A descriptor class defines some combination of get, set, and delete methods. An instance of the descriptor class will be an attribute of the owner class.
Properties are based on the method functions of the owner class. A descriptor, unlike a property, is an instance of a class different from the owning class. Therefore, descriptors are often reusable, generic kinds of attributes. The owning class can have multiple instances of each descriptor class to manage attributes with similar behaviors.
Unlike other attributes, descriptors are created at the class level. They're not created within the __init__()
initialization. While descriptor values can be set during initialization, descriptors are generally built as part of the class, outside any method functions.
Each descriptor object will be an instance of a descriptor class bound to a distinct class-level attribute name when the owner class is defined.
To be recognized as a descriptor, a class must implement any combination of the following three methods.
Descriptor.__get__( self, instance, owner ) → object
: In this method, theinstance
parameter is theself
variable of the object being accessed. Theowner
parameter is the owning class object. If this descriptor is invoked in a class context, theinstance
parameter will get aNone
value. This must return the value of the descriptor.Descriptor.__set__( self, instance, value )
: In this method, theinstance
parameter is theself
variable of the object being accessed. Thevalue
parameter is the new value that the descriptor needs to be set to.Descriptor.__delete__( self, instance )
: In this method, theinstance
parameter is theself
variable of the object being accessed. This method of the descriptor must delete this attribute's value.
Sometimes, a descriptor class will also need an __init__()
method function to initialize the descriptor's internal state.
There are two species of descriptors based on the methods defined, as follows:
- A nondata descriptor: This kind of descriptor defines
__set__()
or__delete__()
or both. It cannot define__get__()
. The nondata descriptor object will often be used as part of some larger expression. It might be a callable object, or it might have attributes or methods of its own. An immutable nondata descriptor must implement__set__()
but may simply raiseAttributeError
. These descriptors are slightly simpler to design because the interface is more flexible. - A data descriptor: This descriptor defines
__get__()
at a minimum. Usually, it defines both__get__()
and__set__()
to create a mutable object. The descriptor can't define any further attributes or methods of this object since the descriptor will largely be invisible. A reference to an attribute that has a value of a data descriptor is delegated to the__get__()
,__set__()
, or__delete__()
methods of the descriptor. These can be tricky to design, so we'll look at them second.
There are a wide variety of use cases for descriptors. Internally, Python uses descriptors for several reasons:
- Under the hood, the methods of a class are implemented as descriptors. These are nondata descriptors that apply the method function to the object and the various parameter values.
- The
property()
function is implemented by creating a data descriptor for the named attribute. - A class method or static method is implemented as a descriptor; this applies to the class instead of an instance of the class.
When we look at object-relational mapping in Chapter 11, Storing and Retrieving Objects via SQLite, we'll see that many of the ORM class definitions make heavy use of descriptors to map Python class definitions to SQL tables and columns.
As we think about the purposes of a descriptor, we must also examine the three common use cases for the data that a descriptor works with as follows:
- The descriptor object has, or acquires, the data. In this case, the descriptor object's
self
variable is relevant and the descriptor is stateful. With a data descriptor, the__get__()
method returns this internal data. With a nondata descriptor, the descriptor has other methods or attributes to access this data. - The owner instance contains the data. In this case, the descriptor object must use the
instance
parameter to reference a value in the owning object. With a data descriptor, the__get__()
method fetches the data from the instance. With a nondata descriptor, the descriptor's other methods access the instance data. - The owner class contains the relevant data. In this case, the descriptor object must use the
owner
parameter. This is commonly used when the descriptor implements a static method or class method that applies to the class as a whole.
We'll take a look at the first case in detail. We'll look at creating a data descriptor with __get__()
and __set__()
methods. We'll also look at creating a nondata descriptor without a __get__()
method.
The second case (the data in the owning instance) shows what the @property
decorator does. The possible advantage that a descriptor has over a conventional property is that it moves the calculations into the descriptor class from the owner class. This tends to fragment class design and is probably not the best approach. If the calculations are truly of epic complexity, then a strategy pattern might be better.
The third case shows how the @staticmethod
and @classmethod
decorators are implemented. We don't need to reinvent those wheels.