The idiomatic implementation

We will now look at how to address the questions of the previous section by using a descriptor that is generic enough as to be applied in any class. Again, this example is not really needed because the requirements do not specify such generic behavior (we haven't even followed the rule of three instances of the similar pattern previously creating the abstraction), but it is shown with the goal of portraying descriptors in action.

Do not implement a descriptor unless there is actual evidence of the repetition we are trying to solve, and the complexity is proven to have paid off.

Now, we will create a generic descriptor that, given a name for the attribute to hold the traces of another one, will store the different values of the attribute in a list.

As we mentioned previously, the code is more than what we need for the problem, but its intention is just to show how a descriptor would help us in this case. Given the generic nature of descriptors, the reader will notice that the logic on it (the name of their method, and attributes) does not relate to the domain problem at hand (a traveler object). This is because the idea of the descriptor is to be able to use it in any type of class, probably on different projects, with the same outcomes.

In order to address this gap, some parts of the code are annotated, and the respective explanation for each section (what it does, and how it relates to the original problem) is described in the following code:

class HistoryTracedAttribute:
def __init__(self, trace_attribute_name) -> None:
self.trace_attribute_name = trace_attribute_name # [1]
self._name = None

def __set_name__(self, owner, name):
self._name = name

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self._name]

def __set__(self, instance, value):
self._track_change_in_value_for_instance(instance, value)
instance.__dict__[self._name] = value

def _track_change_in_value_for_instance(self, instance, value):
self._set_default(instance) # [2]
if self._needs_to_track_change(instance, value):
instance.__dict__[self.trace_attribute_name].append(value)

def _needs_to_track_change(self, instance, value) -> bool:
try:
current_value = instance.__dict__[self._name]
except KeyError: # [3]
return True
return value != current_value # [4]

def _set_default(self, instance):
instance.__dict__.setdefault(self.trace_attribute_name, []) # [6]


class Traveller:

current_city = HistoryTracedAttribute("cities_visited") # [1]

def __init__(self, name, current_city):
self.name = name
self.current_city = current_city # [5]

Some annotations and comments on the code are as follows (numbers in the list correspond to the number annotations in the previous listing):

  1. The name of the attribute is one of the variables assigned to the descriptor, in this case, current_city. We pass to the descriptor the name of the variable in which it will store the trace for the variable of the descriptor. In this example, we are telling our object to keep track of all the values that current_city has had in the attribute named cities_visited.
  2. The first time we call the descriptor, in the init, the attribute for tracing values will not exist, in which case we initialize it to an empty list to later append values to it.
  3. In the init method, the name of the attribute current_city will not exist either, so we want to keep track of this change as well. This is the equivalent of initializing the list with the first value in the previous example.
  4. Only track changes when the new value is different from the one that is currently set.
  5. In the init method, the descriptor already exists, and this assignment instruction triggers the actions from step 2 (create the empty list to start tracking values for it), and step 3 (append the value to this list, and set it to the key in the object for retrieval later).
  6. The setdefault method in a dictionary is used to avoid a KeyError. In this case an empty list will be returned for those attributes that aren't still available (see https://docs.python.org/3.6/library/stdtypes.html#dict.setdefault for reference).

It is true that the code in the descriptor is rather complex. On the other hand, the code in the client class is considerably simpler. Of course, this balance only pays off if we are going to use this descriptor multiple times, which is a concern we have already covered.

What might not be so clear at this point is that the descriptor is indeed completely independent from the client class. Nothing in it suggests anything about the business logic. This makes it perfectly suitable to apply it in any other class; even if it does something completely different, the descriptor will take the same effect.

This is the true Pythonic nature of descriptors. They are more appropriate for defining libraries, frameworks, or internal APIs, and not that much for business logic.

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

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