Python > Advanced Python Concepts > Descriptors > Understanding Descriptors (`__get__`, `__set__`, `__delete__`)
Advanced Descriptor: Unit Conversion
This snippet demonstrates a more advanced use of descriptors for unit conversion. It defines a descriptor that automatically converts temperatures between Celsius and Fahrenheit.
Code Implementation
The `TemperatureConverter` descriptor class takes a `to_units` argument specifying the target unit ('celsius' or 'fahrenheit'). The `__get__` method converts Fahrenheit to Celsius if `to_units` is 'celsius', otherwise it returns the Fahrenheit value. The `__set__` method converts Celsius to Fahrenheit if `to_units` is 'celsius' during assignment. The `Thermometer` class uses the descriptor for its `fahrenheit` attribute, demonstrating automatic unit conversion.
class TemperatureConverter:
def __init__(self, to_units='celsius'):
self.to_units = to_units
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
if self.to_units == 'celsius':
return (instance.__dict__[self.name] - 32) * 5 / 9
else:
return instance.__dict__[self.name]
def __set__(self, instance, value):
if self.to_units == 'celsius':
instance.__dict__[self.name] = value * 9 / 5 + 32
else:
instance.__dict__[self.name] = value
class Thermometer:
fahrenheit = TemperatureConverter(to_units='fahrenheit')
def __init__(self, fahrenheit):
self.fahrenheit = fahrenheit
# Example Usage:
therm = Thermometer(77)
print(f'{therm.fahrenheit=}')
print(f'{therm.fahrenheit=:.2f}')
therm.fahrenheit = 32 # Set it to freezing
print(f'{therm.fahrenheit=}')
print(f'{therm.fahrenheit=:.2f}')
Concepts Behind the Snippet
This snippet showcases how descriptors can be used to abstract away complex transformations. The core idea is to encapsulate the unit conversion logic within the descriptor, making the `Thermometer` class cleaner and more focused on its primary purpose: managing temperature values. This approach promotes code reusability and maintainability, as the conversion logic is centralized and can be easily updated or extended without modifying the `Thermometer` class.
Real-Life Use Case
Consider a system that deals with multiple currencies. A descriptor can be used to automatically convert between currencies when accessing or assigning values to attributes, ensuring consistent and accurate financial calculations.
class CurrencyConverter:
def __init__(self, to_currency='USD'):
self.to_currency = to_currency
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
# Simplified example, replace with actual conversion logic
if self.to_currency == 'USD':
return instance.__dict__[self.name] * 1.1 # Simulate conversion
else:
return instance.__dict__[self.name]
def __set__(self, instance, value):
# Simplified example, replace with actual conversion logic
if self.to_currency == 'USD':
instance.__dict__[self.name] = value / 1.1 # Simulate conversion
else:
instance.__dict__[self.name] = value
class Product:
price_eur = CurrencyConverter(to_currency='USD')
def __init__(self, price_eur):
self.price_eur = price_eur
product = Product(100)
print(f'{product.price_eur=}')
Best Practices
Interview Tip
Be prepared to discuss the trade-offs between using descriptors and other approaches, such as properties or custom methods. Explain when descriptors are the most appropriate choice and why. Also, be ready to discuss the performance implications of using descriptors.
When to Use Them
Use descriptors when you need to apply complex logic to attribute access, such as unit conversions, data validation, or lazy initialization. They are particularly useful when you need to enforce consistency across multiple attributes or classes.
Memory Footprint
The memory footprint of this example depends on the complexity of the conversion logic and whether caching is used. If caching is employed, it can increase the memory footprint, but it can also improve performance by avoiding redundant computations. The descriptor object itself has a small memory footprint.
Alternatives
Pros
Cons
FAQ
-
How can I handle exceptions within a descriptor?
You can use try-except blocks within the `__get__` or `__set__` methods to handle exceptions that may occur during the conversion process. Make sure to provide informative error messages to help users diagnose and resolve any issues. -
Can I use descriptors with inheritance?
Yes, descriptors can be used with inheritance. When a descriptor is defined in a base class, it will be inherited by all subclasses. However, you may need to override the descriptor methods in subclasses to customize the behavior for specific attributes.