Hone logo
Hone
Problems

Mastering Python Property Decorators

Property decorators in Python offer a clean and Pythonic way to manage attribute access. They allow you to define methods that behave like attributes, providing control over getter, setter, and deleter operations without explicit method calls. This challenge will help you understand and implement these powerful decorators.

Problem Description

Your task is to implement a custom property decorator in Python that mimics the behavior of Python's built-in property() function. This custom decorator should allow you to define methods for getting, setting, and deleting an attribute, encapsulating data and logic gracefully.

You need to create a class CustomProperty that acts as a decorator. This class should:

  1. Handle Getters: When the decorated attribute is accessed, the getter method (defined using @<property_name>.getter) should be called to return the attribute's value.
  2. Handle Setters: When the decorated attribute is assigned a value, the setter method (defined using @<property_name>.setter) should be called to update the attribute's value.
  3. Handle Deleters: When the decorated attribute is deleted, the deleter method (defined using @<property_name>.deleter) should be called to perform any cleanup or associated actions.
  4. Attribute Storage: The actual data for the property should be stored internally, typically prefixed with an underscore (e.g., _attribute_name).
  5. Initialization: The property should be initialized with a default value when the class is instantiated.

You will then create a class that uses your CustomProperty decorator to manage its attributes.

Examples

Example 1:

class Circle:
    def __init__(self, radius):
        self._radius = radius  # Internal storage

    @CustomProperty.getter
    def radius(self):
        """Getter for the radius."""
        return self._radius

    @radius.setter
    def radius(self, value):
        """Setter for the radius."""
        if value < 0:
            raise ValueError("Radius cannot be negative.")
        self._radius = value

    @radius.deleter
    def radius(self):
        """Deleter for the radius."""
        print("Radius deleted.")
        del self._radius

# --- Usage ---
my_circle = Circle(5)
print(my_circle.radius)
my_circle.radius = 10
print(my_circle.radius)
del my_circle.radius
# This would raise an AttributeError if accessed after deletion
# print(my_circle.radius)

Output:

5
10
Radius deleted.

Explanation: The radius attribute is accessed, and the getter returns the initial _radius value (5). The radius is then set to 10 using the setter, which includes validation. Finally, the deleter is invoked when del my_circle.radius is called.

Example 2:

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @CustomProperty.getter
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError("Temperature must be a number.")
        self._celsius = float(value)

    @property
    def fahrenheit(self):
        return (self._celsius * 9/5) + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self._celsius = (value - 32) * 5/9

# --- Usage ---
temp = Temperature(25)
print(f"{temp.celsius}°C")
print(f"{temp.fahrenheit}°F")

temp.fahrenheit = 77
print(f"{temp.celsius}°C")
print(f"{temp.fahrenheit}°F")

# --- Edge Case ---
try:
    temp.celsius = "hot"
except TypeError as e:
    print(e)

Output:

25.0°C
77.0°F
25.0°C
77.0°F
Temperature must be a number.

Explanation: This example demonstrates how the custom property can interact with other properties (both custom and built-in property). The fahrenheit property is calculated based on celsius and can also be used to set celsius, showing a practical application of managing related data. The TypeError is caught as expected when attempting to set celsius with a non-numeric value.

Constraints

  • Your CustomProperty class must be designed to be used as a decorator.
  • The getter, setter, and deleter methods must be associated with the property name.
  • The internal attribute storage should use a convention like _<attribute_name>.
  • The CustomProperty decorator should handle the case where only a getter is provided.
  • Performance should be reasonable for typical attribute access and modification; avoid unnecessary overhead.

Notes

  • Think about how decorators work in Python. A decorator is essentially a function that takes another function as input and returns a new function. Your CustomProperty class will need to implement the __get__, __set__, and __delete__ methods to achieve the desired attribute behavior.
  • Consider how you will link the getter, setter, and deleter methods to the property name. You might use nested classes or helper functions.
  • The built-in property() function returns a property object. Your CustomProperty should also return an object that can be managed by Python's attribute access mechanism.
  • You can use functools.wraps to preserve the original function's metadata (like name and docstring) for your getter, setter, and deleter methods.
  • The provided examples use a CustomProperty class. You are free to structure your implementation as long as it achieves the described functionality.
Loading editor...
python