Using a data descriptor

A data descriptor is used to build property-like processing using external class definitions. The descriptor methods of __get__(), __set__(), and __delete__() correspond to the way @property can be used to build getter, setter, and deleter methods. The important distinction of the descriptor is a separate and reusable class definition, allowing reuse of property definitions.

We'll design an overly simplistic unit conversion schema using descriptors that can perform appropriate conversions in their __get__() and __set__() methods.

The following is a superclass of a descriptor of units that will do conversions to and from a standard unit:

class Conversion:
"""Depends on a standard value."""
conversion: float
standard: str

def __get__(self, instance: Any, owner: type) -> float:
return getattr(instance, self.standard) * self.conversion

def __set__(self, instance: Any, value: float) -> None:
setattr(instance, self.standard, value / self.conversion)

class Standard(Conversion):
"""Defines a standard value."""
conversion = 1.0

The Conversion class does simple multiplications and divisions to convert standard units to other non-standard units, and vice versa. This doesn't work for temperature conversions, and a subclass is required to handle that case.

The Standard class is an extension to the Conversion class that sets a standard value for a given measurement without any conversion factor being applied. This exists mostly to provide a very visible name to the standard for any particular kind of measurement.

With these two superclasses, we can define some conversions from a standard unit. We'll look at the measurement of speed. Some concrete descriptor class definitions are as follows:

class Speed(Conversion):
standard = "standard_speed" # KPH


class KPH(Standard, Speed):
pass


class Knots(Speed):
conversion = 0.5399568


class MPH(Speed):
conversion = 0.62137119

The abstract Speed class provides the standard source data for the various conversion subclasses, KPHKnots, and MPH. Any attributes based on subclasses of the Speed class will consume standard values.

The KPH class is defined as a subclass of both Standard class and the Speed class. From Standard, it gets a conversion factor of 1.0. From Speed, it gets the attribute name to be used to keep the standard value for speed measurements.

The other classes are subclasses of Speed, which performs conversions from a standard value to the desired value.

The following Trip class uses these conversions for a given measurement:

class Trip:
kph = KPH()
knots = Knots()
mph = MPH()

def __init__(
self,
distance: float,
kph: Optional[float] = None,
mph: Optional[float] = None,
knots: Optional[float] = None,
) -> None:
self.distance = distance # Nautical Miles
if kph:
self.kph = kph
elif mph:
self.mph = mph
elif knots:
self.knots = knots
else:
raise TypeError("Impossible arguments")
self.time = self.distance / self.knots

def __str__(self) -> str:
return (
f"distance: {self.distance} nm, "
f"rate: {self.kph} "
f"kph = {self.mph} "
f"mph = {self.knots} knots, "
f"time = {self.time} hrs"
)

Each of the class-level attributes, kph, knots, and mph, are descriptors for a different unit. When these attributes are referenced, the __get__() and __set__() methods of the various descriptors will perform appropriate conversions to and from the standard values.

The following is an example of an interaction with the Trip class:

>>> m2 = Trip(distance=13.2, knots=5.9)
>>> print(m2)
distance: 13.2 nm, rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots, time = 2.23728813559322 hrs
>>> print(f"Speed: {m2.mph:.3f} mph")
Speed: 6.790 mph
>>> m2.standard_speed
10.92680006993152

We created an object of the Trip class by setting an attribute, distance, setting one of the available descriptors, and then computing a derived value, time. In this example, we set the knots descriptor. This is a subclass of the Speed class, which is a subclass of the Conversion class, and therefore, the value will be converted to a standard value.

When we displayed the value as a large string, each of the descriptors' __get__() methods were used. These methods fetched the internal kph attribute value from the owning object, applied a conversion factor, and returned the resulting values.

The process of creating the descriptors allows for reuse of the essential unit definitions. The calculations can be stated exactly once, and they are separate from any particular application class definition. Compare this with a @property method that is tightly bound to the class including it. The various conversion factors, similarly, are stated once, and can be widely reused by a number of related applications. 

The core description, conversion, embodies a relatively simple computation. When the computation is more complex, it can lead to a sweeping simplification of the overall application. Descriptors are very popular when working with databases and data serialization problems because the descriptor's code can involve complex conversions to different representations.

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

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