Decorator for a method that caches return value after first access
Asked Answered
D

9

30

My problem, and why

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.

What I tried

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)

The question

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.

Dittany answered 18/4, 2016 at 1:55 Comment(5)
Why not just make a custom descriptor that returns the cached object...? A decorator will work, but it seems like the wrong tool for what you're trying to do.Orazio
Do you want this to be an instance attribute, or a class attribute? Test.test would be the syntax for a class attribute, but it sounds like you want this to be an instance thing.Clamber
Note that Django ships with such a decorator, its source is here: docs.djangoproject.com/en/1.9/_modules/django/utils/functional/… . It overrides instance.__dict__ the first time get() is called so that the value can overwrite the property.Seclusion
FTR: this principle of caching method return values after the first call is called memoization.Amoroso
@oliver: but this goes a bit further than mere memoization, as the method is actually replaced by the value as an optimization. It also means that the method can't take any arguments, or it wouldn't work.Seclusion
B
10

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'
>>>

"""

Broadway answered 18/4, 2016 at 2:42 Comment(4)
Accepting this as it is close to my original code. This shows me what I did wrong rather than just an alternate solution. I definitely appreciate all the other answers, I'll keep them all close at hand.Dittany
How can we adapt this for new-style classes? If I have Test derive from object, I get "AttributeError: can't set attribute" with the above code. I guess in new-style classes, the methods are read-only?Isobar
Audrey "cookiecutter" Greenfeld's husband Danny "Two Scoops" has written a Python package to do just this: github.com/pydanny/cached-propertyBrasilin
This solution seems to only work in python 2. I'm getting a AttributeError: can't set attribute error for python 3.8.Mcneill
C
29

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
Cutlerr answered 18/4, 2016 at 2:35 Comment(6)
This is slower than what he described, this still calls a function and does a cache lookup each time the property is accessed after the first.Seclusion
@Seclusion true, but typically that's premature optimization. The high cost of computing the attribute is usually the primary cost in performance, for instance if computing the property hits IO. The ability to do this with builtin python means you should almost always try this first to see if it solves your problem.Cutlerr
You start your answer with "If it's just performance you're concerned with" even though your answer is slower than the solution OP describes (and more complicated, as it involves introducing a cache). Bit strange to call my comment "premature" imo, you simply don't answer the question.Seclusion
@Seclusion OP is asking how to cache return value lazily. OP has store return value as an attribute as their attempted solution. store return value as an attribute is a fair answer, but so are other solutions, which solve OP's problem, and are relevant to the question A decorator for a class method that caches the return after first runCutlerr
@Seclusion but you're right about performance, so edited to remove "If it's just performance you're concerned with" because it is misleading.Cutlerr
BEWARE that this will keep a lru_cache through-out the lifetime of your process and since it has references to each instance of this class they will also be kept (hindering garbage collection, likely until the process is done). This can be VERY BAD for memory usage. As mentioned earlier it will also need to hash self and use that to lookup in the lru_cache dictionary on each access. See rednafi.github.io/reflections/…Fanjet
B
10

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'
>>>

"""

Broadway answered 18/4, 2016 at 2:42 Comment(4)
Accepting this as it is close to my original code. This shows me what I did wrong rather than just an alternate solution. I definitely appreciate all the other answers, I'll keep them all close at hand.Dittany
How can we adapt this for new-style classes? If I have Test derive from object, I get "AttributeError: can't set attribute" with the above code. I guess in new-style classes, the methods are read-only?Isobar
Audrey "cookiecutter" Greenfeld's husband Danny "Two Scoops" has written a Python package to do just this: github.com/pydanny/cached-propertyBrasilin
This solution seems to only work in python 2. I'm getting a AttributeError: can't set attribute error for python 3.8.Mcneill
D
9

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
Denunciatory answered 18/9, 2021 at 21:20 Comment(0)
O
3

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.

Orazio answered 18/4, 2016 at 2:47 Comment(4)
Off course he can, but why would he? That property should only have one responsibility and that is to return what is supposed to.Outtalk
@Outtalk point taken. I'm not sure there's a right answer here but I definitely see the argument that a custom descriptor makes it clear what is trying to be done, whereas using property this way is a bit of an abuse/expansion of its purpose.Orazio
True, a descriptor seems like the way to go especially if you need the instance to operate on or use inheritance to extend what it does in a reusable way.Outtalk
@Outtalk yeah the inheritance issue probably makes a descriptor make even more sense.Orazio
S
3

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.

Seclusion answered 18/4, 2016 at 11:44 Comment(2)
This is really good. Thanks for this. I didn't know Django implemented this kind of thing, I thought it was just web framework stuff. I'll keep the fact that Django has some useful functions in mind for future reference :)Dittany
It turns out that while writing web framework stuff, you often need some handy utils :-)Seclusion
O
2

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.

Outtalk answered 18/4, 2016 at 2:26 Comment(0)
H
0
@functools.lru_cache()
def func(....):
    ....

Reference: @functools.lru_cache() | Python

Highmuckamuck answered 21/2, 2021 at 20:50 Comment(0)
P
0

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

Planimeter answered 30/5, 2022 at 7:51 Comment(0)
W
0

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.

Wellmannered answered 23/1 at 16:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.