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. 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: 123456789101112131415161718192021 class ValidatedRating: def __init__(self, min_val=1.0, max_val=5.0): self.min_val = min_val self.max_val = max_val def __set_name__(self, owner, name): # Automatically save the attribute name (e.g., 'rating') self.public_name = name self.private_name = f"_{name}" def __get__(self, obj, objtype=None): if obj is None: return self return getattr(obj, self.private_name) def __set__(self, obj, value): if not isinstance(value, (int, float)): raise TypeError(f"{self.public_name} must be a number.") if not (self.min_val <= value <= self.max_val): raise ValueError(f"{self.public_name} must be between {self.min_val} and {self.max_val}.") setattr(obj, self.private_name, float(value)) 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: class ComputedUSDPrice: def __init__(self, exchange_rate=0.0067): # 1 JPY = ~0.0067 USD self.exchange_rate = exchange_rate def __set_name__(self, owner, name): self.name = name def __get__(self, obj, objtype=None): if obj is None: return self # Fetch the base JPY price from the parent object jpy_price = getattr(obj, 'price_jpy', 0) return round(jpy_price * self.exchange_rate, 2) def __set__(self, obj, value): raise AttributeError(f"{self.name} is a computed read-only attribute.") 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: 123456789101112131415161718192021222324 import timeclass LazyItinerary: def __set_name__(self, owner, name): self.name = name def __get__(self, obj, objtype=None): if obj is None: return self print(f"[System] Simulating expensive database query for {obj.destination} itinerary...") time.sleep(1) # Simulate network/database latency # Generate the heavy payload itinerary = ( f"Day 1: Arrival in {obj.destination}, check-in.\n" f"Day 2: Guided temple tour and local street food tasting.\n" f"Day 3: Shinkansen departure." ) # Cache it directly on the object's dictionary. # Next time obj.itinerary is called, Python finds it here first! obj.__dict__[self.name] = itinerary return itinerary 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. class JapanTour: # Attach our descriptors at the class level rating = ValidatedRating(min_val=1.0, max_val=5.0) price_usd = ComputedUSDPrice() itinerary = LazyItinerary() def __init__(self, destination, price_jpy, rating): self.destination = destination self.price_jpy = price_jpy self.rating = rating # This triggers ValidatedRating.__set__ 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: # 1. Initializing the tour (Triggers rating validation)tour = JapanTour(destination="Kyoto", price_jpy=45000, rating=4.8)# 2. Testing the Validatorprint(f"Destination: {tour.destination}")print(f"Initial Rating: {tour.rating}")try: tour.rating = 6.0except ValueError as e: print(f"Validation caught error: {e}") 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: Destination: KyotoInitial Rating: 4.8Validation caught error: rating must be between 1.0 and 5.0. Finally, we test the computed descriptor and the lazy descriptor: # 3. Testing the Computed Propertyprint(f"Price (JPY): ¥{tour.price_jpy}")print(f"Price (USD): ${tour.price_usd}")# 4. Testing the Lazy Loaderprint("\n--- Requesting Itinerary (First Time) ---")print(tour.itinerary)print("\n--- Requesting Itinerary (Second Time) ---")print(tour.itinerary) Output: --- Requesting Itinerary (First Time) ---[System] Simulating expensive database query for Kyoto itinerary...Day 1: Arrival in Kyoto, check-in.Day 2: Guided temple tour and local street food tasting.Day 3: Shinkansen departure.--- Requesting Itinerary (Second Time) ---Day 1: Arrival in Kyoto, check-in.Day 2: Guided temple tour and local street food tasting.Day 3: Shinkansen departure. 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.
Descriptors and Controlled Attributes in Python
Full Article
Original Source
Read the full article at Machinelearningmastery →KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.