The Python Oracle

Is there a decorator to simply cache function return values?

Become part of the top 3% of the developers by applying to Toptal https://topt.al/25cXVn

--

Music by Eric Matyas
https://www.soundimage.org
Track title: Breezy Bay

--

Chapters
00:00 Question
00:30 Accepted answer (Score 52)
02:13 Answer 2 (Score 283)
03:28 Answer 3 (Score 39)
04:47 Answer 4 (Score 36)
05:30 Thank you

--

Full question
https://stackoverflow.com/questions/8151...

Accepted answer links:
https://docs.python.org/dev/library/func...
https://stackoverflow.com/a/5295190/8952...

Answer 2 links:
[@functools.lru_cache(maxsize=100, typed=False)]: http://docs.python.org/dev/library/funct...
[Fibonacci numbers]: http://en.wikipedia.org/wiki/Fibonacci_n...
[PyPI]: https://pypi.python.org/pypi/functools32
[Source code]: https://github.com/MiCHiLU/python-functo...
[PyPI]: https://pypi.python.org/pypi/repoze.lru/
[Source code]: https://github.com/repoze/repoze.lru
[PyPI]: https://pypi.python.org/pypi/pylru/
[Source code]: https://github.com/jlhutch/pylru
[PyPI]: https://pypi.python.org/pypi/backports.f...
[Source code]: https://github.com/jaraco/backports.func...

Answer 3 links:
[here]: http://code.activestate.com/recipes/4968.../

Answer 4 links:
[docs]: https://docs.python.org/dev/library/func...
[one of the early answers]: https://stackoverflow.com/a/9674327/6900...
[docs]: https://docs.python.org/3/library/functo...

--

Content licensed under CC BY-SA
https://meta.stackexchange.com/help/lice...

--

Tags
#python #caching #decorator #memoization

#avk47



ANSWER 1

Score 311


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:

from functools import lru_cache

@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:




ACCEPTED ANSWER

Score 68


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 3

Score 40


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 4

Score 28


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