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

  • When implementing complex transformations, consider using caching to avoid redundant computations, especially if the transformation is expensive.
  • Provide clear documentation for the descriptor, explaining its purpose and how it affects the attribute it manages.
  • Test the descriptor thoroughly to ensure it handles various input values and edge cases correctly.

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

  • Custom Methods: Instead of using a descriptor, you could define custom getter and setter methods for the attribute. However, this approach can lead to code duplication if the conversion logic needs to be applied to multiple attributes or classes.
  • Properties: Properties can be used for simpler transformations, but they may not be suitable for complex logic that requires state management or access to the instance's `__dict__`.

Pros

  • Encapsulation: The conversion logic is encapsulated within the descriptor, making the main class cleaner and more focused.
  • Reusability: The descriptor can be reused across multiple attributes or classes, promoting code reuse.
  • Consistency: The descriptor ensures consistent unit conversions across all attributes that use it.

Cons

  • Complexity: Descriptors can be more complex to understand and implement than simpler alternatives.
  • Performance Overhead: The conversion logic within the descriptor can introduce a slight performance overhead.

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.