Implementing a Cached Property in Python
Cached properties are a powerful tool for optimizing code that performs expensive calculations when accessing a property. They store the result of the calculation and return the cached value on subsequent accesses, avoiding redundant computations. This challenge asks you to implement a decorator that transforms a method into a cached property.
Problem Description
You need to create a decorator called cached_property that, when applied to a method of a class, transforms that method into a property. The first time the property is accessed, the decorated method is called, and its return value is stored. Subsequent accesses to the property return the cached value without re-executing the method. The decorator should handle any exceptions raised by the decorated method and re-raise them appropriately.
Key Requirements:
- The decorator should be applicable to any method of a class.
- The decorated method should only be executed once, on the first access.
- Subsequent accesses should return the cached value.
- Exceptions raised by the decorated method should be handled and re-raised.
- The cached value should be stored as an attribute of the instance.
Expected Behavior:
When the decorated property is accessed for the first time, the decorated method is called, its result is stored as an attribute on the instance (with a name derived from the method name), and the result is returned. Subsequent accesses return the stored attribute value. If the decorated method raises an exception, that exception should be raised when the property is accessed.
Edge Cases to Consider:
- Methods that take arguments (although this challenge focuses on simple methods, consider how the decorator might behave).
- Methods that modify the instance state.
- Methods that raise exceptions.
- Classes with multiple cached properties.
Examples
Example 1:
class MyClass:
def __init__(self, value):
self.value = value
@cached_property
def expensive_calculation(self):
print("Calculating...")
return self.value * 2
my_instance = MyClass(5)
print(my_instance.expensive_calculation) # Output: Calculating... 10
print(my_instance.expensive_calculation) # Output: 10 (no calculation)
Explanation: The first access triggers the calculation and prints "Calculating...". The result (10) is cached. The second access returns the cached value without re-executing the method.
Example 2:
class MyClass:
def __init__(self, value):
self.value = value
@cached_property
def error_prone_calculation(self):
if self.value < 0:
raise ValueError("Value must be non-negative")
return self.value * 2
my_instance = MyClass(5)
print(my_instance.error_prone_calculation) # Output: 10
my_instance2 = MyClass(-1)
try:
print(my_instance2.error_prone_calculation)
except ValueError as e:
print(e) # Output: Value must be non-negative
Explanation: The first instance works fine. The second instance raises a ValueError, which is correctly propagated.
Example 3:
class MyClass:
def __init__(self, value):
self.value = value
@cached_property
def modified_state_calculation(self):
print("Calculating and modifying...")
self.value += 1
return self.value * 2
my_instance = MyClass(5)
print(my_instance.modified_state_calculation) # Output: Calculating and modifying... 12
print(my_instance.value) # Output: 6 (state has been modified)
print(my_instance.modified_state_calculation) # Output: 12 (no calculation, state unchanged)
Explanation: The calculation modifies the instance's value attribute. Subsequent accesses return the cached value and do not re-execute the method, so the state is not modified again.
Constraints
- The decorator must be implemented using Python's built-in features (no external libraries).
- The cached value must be stored as an attribute of the instance.
- The name of the cached attribute should be derived from the name of the decorated method.
- The decorator should work correctly with methods that raise exceptions.
- The decorator should be compatible with any class.
Notes
- Consider using a closure to store the cached value.
- Think about how to handle exceptions raised by the decorated method.
- The decorator should be transparent to the user – it should behave like a regular property.
- The method being decorated should not take any arguments. Focus on the core caching functionality.