How to use the Python decorator pattern to cache the result values of your computationally expensive method calls.
Cache decorators are convenient methods for caching function return values.
Use them like this:
@cache_this_function def my_slow_function(): # This is run only once and all subsequent calls get value from the cache return
Cache decorators do not work with methods or functions that use
The cache will end up storing an empty value.
The plone.memoize package offers helpful function decorators to cache return values.
See also using memcached backend for memoizers.
Cache result for process lifecycle¶
from plone.memoize import forever @forever.memoize def getFields(area, subject): """ Get all fields inside area / subject. Results is cached for process lifetime. @return: List of internal fields """ schema = getSchema(area) return [field for field in schema if field["subject"] == subject]
@ram.cache decorator takes a function argument and calls it to get a value.
As long as that value is unchanged, the cached result of the decorated function is returned. This makes it easy to set a timeout cache:
from plone.memoize import ram from time import time @ram.cache(lambda *args: time() // (60 * 60)) def cached_query(self): # very expensive operation, # will not be called more than once an hour
time.time() returns the time in seconds as a floating point number.
// is Python’s integer division.
The result of
time() // (60 * 60) only changes once an hour.
args passed are ignored.
Caching per request¶
This pattern shows how to avoid recalculating the same value repeatedly during the lifecycle of an HTTP request.
Caching on BrowserViews¶
This is useful if the same view/utility is going to be called many times from different places during the same HTTP request.
The plone.memoize.view package provides necessary decorators for
from plone.memoize.view import memoize, memoize_contextless class MyView(BrowserView): @memoize def getValue(): """ This value is recalculated for every new BrowserView context per request. """ return "something" @memoize_contextless def getValueNoContext(): """ This value is recalculated for all context objects once per request. """ return "something"
Caching on Archetypes accessors¶
If you have a custom Archetypes accessor method, you can avoid recalculating it during the request processing.
def getParsedORADataCached(self): """ Same as above, but does not run through JSON reader every time. """ # Manually store the result on HTTP request object annotations # Use informative string + Archetypes unique identified as the key key = "parsed-ora-data-" + self.UID() cache = IAnnotations(self.REQUEST) data = cache.get(key, None) if data is not None: data = self.getParsedORAData() cache[key] = data return data
Caching using global HTTP request¶
This example uses the
for caching. Values are stored on the thread-local
which lasts for the transaction lifecycle:
from zope.globalrequest import getRequest from zope.annotation.interfaces import IAnnotations def _getProductList(self, type, language): """ Private implementation, builds list of products. """ logger.info("Getting product list %s %s" % (type, language)) ... return result def getProductListCached(self, type, language): """ Public cached method, delegates to _getProductList. """ request = getRequest() key = "cache-%s-%s" % (type, language) cache = IAnnotations(request) data = cache.get(key, None) if not data: data = self._getProductList(type, language) cache[key] = data return data
Testing memoized methods inside browser views¶
While testing browser views memoized methods, you could find out that calling a method multiple times inside a test could result in getting the same result over and over, no matter what the parameters are, because you have the same context and request inside the test and the result is being cached.
One approach to bypass this is to put your code logic inside a private method while memoizing a public method with the same name that only calls the private one:
from plone.memoize import view from Products.Five import BrowserView class MyView(BrowserView): def _my_expensive_method(): """Code logic goes here. """ return "something" @view.memoize def my_expensive_method(): """We call the private method here and memoize the result. """ return self._my_expensive_method()
In your tests you can call the private method to avoid memoization.