Zope Cache Descriptors
Zope Cache Descriptors is a Python library from the Zope Foundation providing `cached` method and `CachedProperty` decorators for easy result caching. It's currently at version 6.0, supports Python 3.9+, and sees active maintenance with releases roughly aligning with major Python version support cycles. It's designed for simple, in-memory caching of computation results.
Common errors
-
AttributeError: 'CachedProperty' object has no attribute 'invalidate'
cause Attempting to invalidate a `CachedProperty` using a method-style invalidation call (like `obj.prop.invalidate()`) which is meant for `cached` methods.fixCached properties are invalidated by deleting the attribute: `del obj.your_property`. -
TypeError: cached() missing 1 required positional argument: 'func'
cause Trying to use `@cached()` without parentheses for the decorator, or trying to call the `cached` decorator directly without providing a function.fix`@cached` is a decorator without arguments, so use `@cached` (no parentheses). Ensure it's placed directly above a method definition. -
ImportError: cannot import name 'CachedProperty' from 'zope.cachedescriptors'
cause Incorrect import path. `CachedProperty` and `cached` are in specific submodules, not directly under `zope.cachedescriptors`.fixUse the correct import paths: `from zope.cachedescriptors.property import CachedProperty` and `from zope.cachedescriptors.method import cached`.
Warnings
- breaking Version 6.0 drops support for Python 3.8 and Zope 4. Upgrading on older environments will lead to import errors or runtime issues.
- breaking In version 6.0, `CachedProperty` no longer subclasses Python's built-in `property`. Code relying on `isinstance(obj.my_prop, property)` will now return `False`.
- breaking Starting with version 6.0, `cached` methods (e.g., `obj.my_method`) are now instances of `CachedMethod` (a callable object) rather than the raw function. This can impact introspection or direct manipulation of the underlying function.
- gotcha Cache invalidation is explicit. Forgetting to invalidate a cache entry after the underlying data changes will lead to stale data being returned.
- gotcha If the result of a cached method/property is a mutable object (e.g., list, dict), subsequent modifications to that object will be reflected in all cached accesses. This can lead to unexpected side effects or data corruption.
Install
-
pip install zope-cachedescriptors
Imports
- CachedProperty
from zope.cachedescriptors import CachedProperty
from zope.cachedescriptors.property import CachedProperty
- cached
from zope.cachedescriptors import cached
from zope.cachedescriptors.method import cached
Quickstart
from zope.cachedescriptors.property import CachedProperty
from zope.cachedescriptors.method import cached
import time
class DataFetcher:
def __init__(self, id_val):
self.id = id_val
self._fetch_count = 0
@CachedProperty
def expensive_property(self):
self._fetch_count += 1
print(f" [Property] Fetching data for {self.id}... (call {self._fetch_count})")
time.sleep(0.1) # Simulate network delay
return f"Data for {self.id}-{time.time()}"
@cached
def expensive_method(self, param):
self._fetch_count += 1
print(f" [Method] Fetching data for {self.id} with param {param}... (call {self._fetch_count})")
time.sleep(0.1) # Simulate network delay
return f"Result for {self.id}-{param}-{time.time()}"
# Example Usage
fetcher = DataFetcher("user123")
print("--- CachedProperty Demo ---")
result1 = fetcher.expensive_property
print(f"Property 1: {result1}")
result2 = fetcher.expensive_property # Should be cached
print(f"Property 2: {result2}")
assert result1 == result2
# Invalidate the property cache by deleting the attribute
del fetcher.expensive_property
result3 = fetcher.expensive_property # Should re-calculate
print(f"Property 3 (after invalidation): {result3}")
assert result1 != result3
print("\n--- CachedMethod Demo ---")
method_result1 = fetcher.expensive_method("report_a")
print(f"Method 1: {method_result1}")
method_result2 = fetcher.expensive_method("report_a") # Should be cached
print(f"Method 2: {method_result2}")
assert method_result1 == method_result2
# Invalidate a specific method call with arguments
fetcher.expensive_method.invalidate(fetcher, "report_a")
method_result3 = fetcher.expensive_method("report_a") # Should re-calculate
print(f"Method 3 (after invalidation): {method_result3}")
assert method_result1 != method_result3
method_result4 = fetcher.expensive_method("report_b") # New argument, new cache entry
print(f"Method 4: {method_result4}")