Question :
Consider the following:
@property
def name(self):
if not hasattr(self, '_name'):
# expensive calculation
self._name = 1 + 1
return self._name
I’m new, but I think the caching could be factored out into a decorator. Only I didn’t find one like it 😉
PS the real calculation doesn’t depend on mutable values
Answer #1:
Starting from Python 3.2 there is a built-in decorator:
@functools.lru_cache(maxsize=100, typed=False)
Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function is periodically called with the same arguments.
Example of an LRU cache for computing Fibonacci numbers:
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)
If you are stuck with Python 2.x, here’s a list of other compatible memoization libraries:
functools32
| PyPI | Source coderepoze.lru
| PyPI | Source codepylru
| PyPI | Source codebackports.functools_lru_cache
| PyPI | Source code
Answer #2:
It sounds like you’re not asking for a general-purpose memoization decorator (i.e., you’re not interested in the general case where you want to cache return values for different argument values). That is, you’d like to have this:
x = obj.name # expensive
y = obj.name # cheap
while a general-purpose memoization decorator would give you this:
x = obj.name() # expensive
y = obj.name() # cheap
I submit that the method-call syntax is better style, because it suggests the possibility of expensive computation while the property syntax suggests a quick lookup.
[Update: The class-based memoization decorator I had linked to and quoted here previously doesn’t work for methods. I’ve replaced it with a decorator function.] If you’re willing to use a general-purpose memoization decorator, here’s a simple one:
def memoize(function):
memo = {}
def wrapper(*args):
if args in memo:
return memo[args]
else:
rv = function(*args)
memo[args] = rv
return rv
return wrapper
Example usage:
@memoize
def fibonacci(n):
if n < 2: return n
return fibonacci(n - 1) + fibonacci(n - 2)
Another memoization decorator with a limit on the cache size can be found here.
Answer #3:
class memorize(dict):
def __init__(self, func):
self.func = func
def __call__(self, *args):
return self[args]
def __missing__(self, key):
result = self[key] = self.func(*key)
return result
Sample uses:
>>> @memorize
... def foo(a, b):
... return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}
Answer #4:
Python 3.8 functools.cached_property
decorator
https://docs.python.org/dev/library/functools.html#functools.cached_property
cached_property
from Werkzeug was mentioned at: https://stackoverflow.com/a/5295190/895245 but a supposedly derived version will be merged into 3.8, which is awesome.
This decorator can be seen as caching @property
, or as a cleaner @functools.lru_cache
for when you don’t have any arguments.
The docs say:
@functools.cached_property(func)
Transform a method of a class into a property whose value is computed once and then cached as a normal attribute for the life of the instance. Similar to property(), with the addition of caching. Useful for expensive computed properties of instances that are otherwise effectively immutable.
Example:
class DataSet: def __init__(self, sequence_of_numbers): self._data = sequence_of_numbers @cached_property def stdev(self): return statistics.stdev(self._data) @cached_property def variance(self): return statistics.variance(self._data)
New in version 3.8.
Note This decorator requires that the dict attribute on each instance be a mutable mapping. This means it will not work with some types, such as metaclasses (since the dict attributes on type instances are read-only proxies for the class namespace), and those that specify slots without including dict as one of the defined slots (as such classes don’t provide a dict attribute at all).
Answer #6:
I coded this simple decorator class to cache function responses. I find it VERY useful for my projects:
from datetime import datetime, timedelta
class cached(object):
def __init__(self, *args, **kwargs):
self.cached_function_responses = {}
self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))
def __call__(self, func):
def inner(*args, **kwargs):
max_age = kwargs.get('max_age', self.default_max_age)
if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
if 'max_age' in kwargs: del kwargs['max_age']
res = func(*args, **kwargs)
self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
return self.cached_function_responses[func]['data']
return inner
The usage is straightforward:
import time
@cached
def myfunc(a):
print "in func"
return (a, datetime.now())
@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
print "in cacheable test: "
return (a, datetime.now())
print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))
Answer #7:
DISCLAIMER: I’m the author of kids.cache.
You should check kids.cache
, it provides a @cache
decorator that works on python 2 and python 3. No dependencies, ~100 lines of code. It’s very straightforward to use, for instance, with your code in mind, you could use it like this:
pip install kids.cache
Then
from kids.cache import cache
...
class MyClass(object):
...
@cache # <-- That's all you need to do
@property
def name(self):
return 1 + 1 # supposedly expensive calculation
Or you could put the @cache
decorator after the @property
(same result).
Using cache on a property is called lazy evaluation, kids.cache
can do much more (it works on function with any arguments, properties, any type of methods, and even classes…). For advanced users, kids.cache
supports cachetools
which provides fancy cache stores to python 2 and python 3 (LRU, LFU, TTL, RR cache).
IMPORTANT NOTE: the default cache store of kids.cache
is a standard dict, which is not recommended for long running program with ever different queries as it would lead to an ever growing caching store. For this usage you can plugin other cache stores using for instance (@cache(use=cachetools.LRUCache(maxsize=2))
to decorate your function/property/class/method…)
Answer #8:
Ah, just needed to find the right name for this: “Lazy property evaluation“.
I do this a lot too; maybe I’ll use that recipe in my code sometime.