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:
- 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. - 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. - 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. - Attribute Storage: The actual data for the property should be stored internally, typically prefixed with an underscore (e.g.,
_attribute_name). - 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
CustomPropertyclass 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
CustomPropertydecorator 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
CustomPropertyclass 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. YourCustomPropertyshould also return an object that can be managed by Python's attribute access mechanism. - You can use
functools.wrapsto preserve the original function's metadata (like name and docstring) for your getter, setter, and deleter methods. - The provided examples use a
CustomPropertyclass. You are free to structure your implementation as long as it achieves the described functionality.