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. Descriptors are the essence of how Python implements methods, attributes, and properties.

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 the __get__, __set__, and __delete__ methods. An instance of the descriptor class will be an attribute of the owner class.

A descriptor is an instance of a class that is separate from the owning class. Therefore, descriptors let us create 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 instances can have values set during initialization, the descriptor instances are generally built as part of the class, outside any method functions. Each descriptor object will be an instance of a descriptor class. The descriptor instance must be bound to an attribute name in the owner class.

To be recognized as a descriptor, a class must implement any combination of the following three methods:

  • Descriptor.__get__(self, instance, owner): In this method, the instance parameter is the self variable of the object being accessed. The owner parameter is the owning class object. If this descriptor is invoked in a class context, the instance parameter will get a None value. This must return the value of the descriptor.
  • Descriptor.__set__(self, instance, value): In this method, the instance parameter is the self variable of the object being accessed. The value parameter is the new value that the descriptor needs to be set to.
  • Descriptor.__delete__(self, instance): In this method, the instance parameter is the self 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 design patterns for descriptors based on the methods defined, as follows:

  • A non-data descriptor: This kind of descriptor defines only the __get__() method. The idea of a non-data descriptor is to provide an indirect reference to another object via methods or attributes of its own. A non-data descriptor can also take some action when referenced.
  • A data descriptor: This descriptor defines both __get__() and __set__() to create a mutable object. It may also define __delete__(). A reference to an attribute with a value of a data descriptor is delegated to the __get__(), __set__(), or __delete__() methods of the descriptor object.

There are a wide variety of use cases for descriptors. Internally, Python uses descriptors for several reasons:

  • The methods of a class are implemented as descriptors. These are non-data 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. In both cases, the method will apply to the class instead of an instance of the class.

When we look at object-relational mapping in Chapter 12, Storing and Retrieving Objects via SQLite, we'll see that many of the ORM class definitions make 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 value. In this case, the descriptor object's self variable is relevant, and the descriptor object is stateful. With a data descriptor, the __get__() method can return this internal data. With a non-data descriptor, the descriptor may include other methods or attributes to acquire or process data. Any descriptor state applies to the class as a whole.
  • 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 non-data 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. This means creating a data descriptor with __get__() and __set__() methods. We'll also look at creating a non-data descriptor without a __get__() method.

The second case (the data in the owning instance) is essentially what the @property decorator does. There's a small possible advantage to writing a descriptor class instead of creating a conventional property—a descriptor can be used to refactor the calculations into the descriptor class. While this may fragment class design, it can help when the calculations are truly of epic complexity. This is essentially the Strategy design pattern, a separate class that embodies a particular algorithm.

The third case shows how the @staticmethod and @classmethod decorators are implemented. We don't need to reinvent those wheels.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset