Reputation: 9561
I'm trying to write a decorator for a method, @cachedproperty
. I want it to behave so that when the method is first called, the method is replaced with its return value. I also want it to behave like @property
so that it doesn't need to be explicitly called. Basically, it should be indistinguishable from @property
except that it's faster, because it only calculates the value once and then stores it. My idea is that this would not slow down instantiation like defining it in __init__
would. That's why I want to do this.
First, I tried to override the fget
method of the property
, but it's read-only.
Next, I figured I'd try to implement a decorator that does needs to be called the first time but then caches the values. This isn't my final goal of a property-type decorator that never needs to be called, but I thought this would be a simpler problem to tackle first. In other words, this is a not-working solution to a slightly simpler problem.
I tried:
def cachedproperty(func):
""" Used on methods to convert them to methods that replace themselves
with their return value once they are called. """
def cache(*args):
self = args[0] # Reference to the class who owns the method
funcname = inspect.stack()[0][3] # Name of the function, so that it can be overridden.
setattr(self, funcname, func()) # Replace the function with its value
return func() # Return the result of the function
return cache
However, this doesn't seem work. I tested this with:
>>> class Test:
... @cachedproperty
... def test(self):
... print "Execute"
... return "Return"
...
>>> Test.test
<unbound method Test.cache>
>>> Test.test()
but I get an error about how the class didn't pass itself to the method:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unbound method cache() must be called with Test instance as first argument (got nothing instead)
At this point, me and my limited knowledge of deep Python methods are very confused, and I have no idea where my code went wrong or how to fix it. (I've never tried to write a decorator before)
How can I write a decorator that will return the result of calling a method the first time it's accessed (like @property
does), and be replaced with a cached value for all subsequent queries?
I hope this question isn't too confusing, I tried to explain it as well as I could.
Upvotes: 30
Views: 28185
Reputation: 16561
The existing answers are referring to the old functools.lru_cache
decorator. The new decorator is functools.cache
, but there is an additional consideration to keep in mind: should the caching be at the instance level or at the class level?
If the computation is not instance-specific, then a combination of @property
and @cache
should work well. However, if the method called does depend on the instance, then functools.cached_property
should be used instead.
Here's an example:
from dataclasses import dataclass
from functools import cache
@dataclass(frozen=True)
class A:
a: int
@property
@cache
def test(self):
print("Computing A")
return self.a + 1
a1 = A(a=1)
a2 = A(a=1)
print(a1.test, a2.test)
# Computing A
# 2 2
Note that computation is triggered only once, even though there are two separate instances of the class. By using functools.cached_property
we can trigger instance-specific caching:
from dataclasses import dataclass
from functools import cached_property
@dataclass(frozen=True)
class B:
b: int
@cached_property
def test(self):
print("Computing B")
return self.b + 1
b1 = B(b=1)
b2 = B(b=1)
print(b1.test, b2.test)
# Computing B
# Computing B
# 2 2
Note that computation is triggered for every instance.
Upvotes: 0
Reputation: 1147
Have u tried djangos built in: from django.utils.functional import cached_property
please don't use lru_cache as suggested by multiple people as it opens up a host of possible memory leak issues
Upvotes: 0
Reputation: 115
With Python 3.8 or later you can use functools.cached_property().
It works similar as the previously proposed lru_cache solution.
Example usage:
import functools
class Test:
@functools.cached_property
def calc(self):
print("Calculating")
return 1
Test output:
In [2]: t = Test()
In [3]: t.calc
Calculating
Out[3]: 1
In [4]: t.calc
Out[4]: 1
Upvotes: 9
Reputation: 902
@functools.lru_cache()
def func(....):
....
Reference: @functools.lru_cache() | Python
Upvotes: 0
Reputation: 4057
If you don't mind alternative solutions, I'd recommend lru_cache
for example
from functools import lru_cache
class Test:
@property
@lru_cache(maxsize=None)
def calc(self):
print("Calculating")
return 1
Expected output
In [2]: t = Test()
In [3]: t.calc
Calculating
Out[3]: 1
In [4]: t.calc
Out[4]: 1
Upvotes: 29
Reputation: 31260
Django's version of this decorator does exactly what you describe and is simple, so besides my comment I'll just copy it here:
class cached_property(object):
"""
Decorator that converts a method with a single self argument into a
property cached on the instance.
Optional ``name`` argument allows you to make cached properties of other
methods. (e.g. url = cached_property(get_absolute_url, name='url') )
"""
def __init__(self, func, name=None):
self.func = func
self.__doc__ = getattr(func, '__doc__')
self.name = name or func.__name__
def __get__(self, instance, type=None):
if instance is None:
return self
res = instance.__dict__[self.name] = self.func(instance)
return res
(source).
As you can see, it uses func.name to determine the name of the function (no need to fiddle with inspect.stack) and it replaces the method with its result by mutating instance.__dict__
. So subsequent "calls" are just an attribute lookup and there is no need for any caches, et cetera.
Upvotes: 3
Reputation: 45251
I think you're better off with a custom descriptor, since this is exactly the kind of thing descriptors are for. Like so:
class CachedProperty:
def __init__(self, name, get_the_value):
self.name = name
self.get_the_value = get_the_value
def __get__(self, obj, typ):
name = self.name
while True:
try:
return getattr(obj, name)
except AttributeError:
get_the_value = self.get_the_value
try:
# get_the_value can be a string which is the name of an obj method
value = getattr(obj, get_the_value)()
except AttributeError:
# or it can be another external function
value = get_the_value()
setattr(obj, name, value)
continue
break
class Mine:
cached_property = CachedProperty("_cached_property ", get_cached_property_value)
# OR:
class Mine:
cached_property = CachedProperty("_cached_property", "get_cached_property_value")
def get_cached_property_value(self):
return "the_value"
EDIT: By the way, you don't even actually need a custom descriptor. You could just cache the value inside of your property function. E.g.:
@property
def test(self):
while True:
try:
return self._test
except AttributeError:
self._test = get_initial_value()
That's all there is to it.
However, many would consider this a bit of an abuse of property
, and to be an unexpected way of using it. And unexpected usually means you should do it another, more explicit way. A custom CachedProperty
descriptor is very explicit, so for that reason I would prefer it to the property
approach, though it requires more code.
Upvotes: 3
Reputation: 2035
First of all Test
should be instantiated
test = Test()
Second, there is no need for inspect
cause we can get the property name from func.__name__
And third, we return property(cache)
to make python to do all the magic.
def cachedproperty(func):
" Used on methods to convert them to methods that replace themselves\
with their return value once they are called. "
def cache(*args):
self = args[0] # Reference to the class who owns the method
funcname = func.__name__
ret_value = func(self)
setattr(self, funcname, ret_value) # Replace the function with its value
return ret_value # Return the result of the function
return property(cache)
class Test:
@cachedproperty
def test(self):
print "Execute"
return "Return"
>>> test = Test()
>>> test.test
Execute
'Return'
>>> test.test
'Return'
>>>
"""
Upvotes: 10
Reputation: 4379
You can use something like this:
def cached(timeout=None):
def decorator(func):
def wrapper(self, *args, **kwargs):
value = None
key = '_'.join([type(self).__name__, str(self.id) if hasattr(self, 'id') else '', func.__name__])
if settings.CACHING_ENABLED:
value = cache.get(key)
if value is None:
value = func(self, *args, **kwargs)
if settings.CACHING_ENABLED:
# if timeout=None Django cache reads a global value from settings
cache.set(key, value, timeout=timeout)
return value
return wrapper
return decorator
When adding to the cache dictionary it generates keys based on the convention class_id_function
in case you are caching entities and the property could possibly return a different value for each one.
It also checks a settings key CACHING_ENABLED
in case you want to turn it off temporarily when doing benchmarks.
But it does not encapsulate the standard property
decorator so you should still call it like a function, or you can use it like this (why restrict it to properties only):
@cached
@property
def total_sales(self):
# Some calculations here...
pass
Also it may be worth noting that in case you are caching a result from lazy foreign key relationships, there are times depending on your data where it would be faster to simply run an aggregate function when doing your select query and fetching everything at once, than visiting the cache for every record in your result-set. So use some tool like django-debug-toolbar
for your framework to compare what performs best in your scenario.
Upvotes: 2