Descriptors and Controlled Attributes in Python

In this article, you will learn what Python descriptors are and how to use them to build three types of controlled attributes: validated, computed, and lazily loaded.

Topics we will cover include:

  • How to build a validated attribute descriptor that enforces type and range constraints on class attributes.
  • How to build a computed attribute descriptor that derives its value dynamically from another attribute.
  • How to build a lazily loaded attribute descriptor that defers expensive computations until the attribute is first accessed.

Descriptors and Controlled Attributes

Introduction and Motivation

In Python, descriptors are a mechanism that governs what occurs when we access an object’s attribute, set it, or remove it. They are defined through a powerful protocol whereby a class implements special methods like __get__, __set__, and so on, to customize access to the attribute(s).

Meanwhile, controlled attributes are properties defined at the class level whose write, read, and delete logic is handled by the aforesaid descriptors. Mastering descriptors is the key to understanding the mechanics behind many well-known Python libraries and configuration systems. We don’t intend to go that deep in this article, but we will show you how to use Python to build three types of controlled attributes through descriptors: computed, validated, and lazily loaded attributes.

And how will we learn this? Through an example that simulates tracking data in a fictitious Japan tourism application, showing that descriptors can be used to neatly manage attribute states without resorting to code-cluttering @property decorators, getters, and setters.

1. Building Validated Attributes through Descriptors

First and foremost, in Python programming terms, a descriptor is a class that implements methods like __get__, __set__, or __delete__ to define behavior at the attribute level. Not all of them need to be implemented, as we will see. There is also another method introduced in Python 3.6, called __set_name__, which lets the descriptor know the name of the attribute it is being assigned to — something useful for storing data inside a parent object’s __dict__ while avoiding name clashes.

In a nutshell, the following code defines a descriptor called ValidatedRating that enables easy and robust validation of data inputs on class attributes, verifying in particular that assigned ratings meet specific type and range requirements:

We will see this descriptor in action together with the other two once we have defined them all.

2. Building Computed Attributes through Descriptors

Computed attribute descriptors are useful in situations like dynamically calculating an attribute’s value based on another object’s state. For instance, if in our Japan tourism app (implemented shortly) we want prices stored in Japanese Yen (JPY) but need to display them in US Dollars, a descriptor can become particularly handy — specifically by implementing a __get__ method that calculates the currency conversion on the fly:

3. Building Lazily Loaded Attributes through Descriptors

The third strategy is useful for attributes that require expensive computations, e.g. reading a large JSON file or querying a large database for Shinkansen (Japanese bullet train) timetables. In these cases, lazily loaded attributes simply defer loading until the data is explicitly requested. A descriptor based on this policy, also called a lazy descriptor, calculates the value on first access, saves it to the __dict__ of your instance, and steps aside. After that, future access bypasses the descriptor entirely.

The following class implements lazy loading, caching data that might be costly to generate or retrieve:

Putting It All Together

Let’s see our three descriptors in action! The following code applies them to a class named JapanTour, whose attributes are each governed by one of our three custom descriptors.

Now that we have a tangible class with real-world-like attributes, the abstract descriptors we defined earlier make a lot more sense. In the constructor, any attribute backed by a descriptor that defines __set__ will invoke that method immediately during initialization.

Let’s instantiate a JapanTour object and run a first test:

Not only do we access attributes, but we also try assigning an invalid value — one that exceeds the range enforced by our validated rating descriptor — which results in an exception being caught:

Finally, we test the computed descriptor and the lazy descriptor:

Output:

Notice that on the second request, there is no system loading message — the lazy descriptor has already cached the result and Python reads it directly from the instance’s __dict__.

Wrapping Up

We have shown how to make custom class attributes significantly more robust in real-world applications using Python descriptors. The three types covered — validated, computed, and lazily loaded — address data integrity constraints, dynamically derived values, and expensive data operations, respectively.

No comments yet.

Leave a Reply

Machine Learning Mastery is part of Guiding Tech Media, a leading digital media publisher focused on helping people figure out technology. Visit our corporate website to learn more about our mission and team.