36

My question is are the following two pieces of code run the same by the interpreter:

class A(object):
  def __init__(self):
     self.__x = None

  @property
  def x(self):
     if not self.__x:
        self.__x = ... #some complicated action
     return self.__x

and the much simpler:

class A(object):
  @property
  def x(self):
      return ... #some complicated action

I.e., is the interpreter smart enough to cache the property x?

My assumption is that x does not change - finding it is hard, but once you find it once there is no reason to find it again.

Guy
  • 14,178
  • 27
  • 67
  • 88

8 Answers8

34

No, the getter will be called every time you access the property.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
23

To anyone who might be reading this in 2020, this functionality is now available in the functools module as part of the standard library as of Python 3.8.

https://docs.python.org/dev/library/functools.html#functools.cached_property

Important to note, classes that define their own __dict__ (or do not define one at all) or use __slots__ might not work as expected. For example, NamedTuple and metaclasses.

Alois Mahdal
  • 10,763
  • 7
  • 51
  • 69
Tyler Donaldson
  • 426
  • 3
  • 6
19

No you need to add a memoize decorator:

class memoized(object):
   """Decorator that caches a function's return value each time it is called.
   If called later with the same arguments, the cached value is returned, and
   not re-evaluated.
   """
   def __init__(self, func):
      self.func = func
      self.cache = {}
   def __call__(self, *args):
      try:
         return self.cache[args]
      except KeyError:
         value = self.func(*args)
         self.cache[args] = value
         return value
      except TypeError:
         # uncachable -- for instance, passing a list as an argument.
         # Better to not cache than to blow up entirely.
         return self.func(*args)
   def __repr__(self):
      """Return the function's docstring."""
      return self.func.__doc__
   def __get__(self, obj, objtype):
      """Support instance methods."""
      return functools.partial(self.__call__, obj)

@memoized
def fibonacci(n):
   "Return the nth fibonacci number."
   if n in (0, 1):
      return n
   return fibonacci(n-1) + fibonacci(n-2)

print fibonacci(12)
fabrizioM
  • 46,639
  • 15
  • 102
  • 119
17

Properties do not automatically cache their return values. The getter (and setters) are intended to be called each time the property is accessed.

However, Denis Otkidach has written a wonderful cached attribute decorator (published in the Python Cookbook, 2nd edition and also originally on ActiveState under the PSF license) for just this purpose:

class cache(object):    
    '''Computes attribute value and caches it in the instance.
    Python Cookbook (Denis Otkidach) https://stackoverflow.com/users/168352/denis-otkidach
    This decorator allows you to create a property which can be computed once and
    accessed many times. Sort of like memoization.

    '''
    def __init__(self, method, name=None):
        # record the unbound-method and the name
        self.method = method
        self.name = name or method.__name__
        self.__doc__ = method.__doc__
    def __get__(self, inst, cls):
        # self: <__main__.cache object at 0xb781340c>
        # inst: <__main__.Foo object at 0xb781348c>
        # cls: <class '__main__.Foo'>       
        if inst is None:
            # instance attribute accessed on class, return self
            # You get here if you write `Foo.bar`
            return self
        # compute, cache and return the instance's attribute value
        result = self.method(inst)
        # setattr redefines the instance's attribute so this doesn't get called again
        setattr(inst, self.name, result)
        return result

Here is an example demonstrating its use:

def demo_cache():
    class Foo(object):
        @cache
        def bar(self):
            print 'Calculating self.bar'  
            return 42
    foo=Foo()
    print(foo.bar)
    # Calculating self.bar
    # 42
    print(foo.bar)    
    # 42
    foo.bar=1
    print(foo.bar)
    # 1
    print(Foo.bar)
    # __get__ called with inst = None
    # <__main__.cache object at 0xb7709b4c>

    # Deleting `foo.bar` from `foo.__dict__` re-exposes the property defined in `Foo`.
    # Thus, calling `foo.bar` again recalculates the value again.
    del foo.bar
    print(foo.bar)
    # Calculating self.bar
    # 42

demo_cache()
Community
  • 1
  • 1
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
15

Python 3.2 onwards offers a built-in decorator that you can use to create a LRU cache:

@functools.lru_cache(maxsize=128, typed=False)

Alternatively, if you're using Flask / Werkzeug, there's the @cached_property decorator.

For Django, try from django.utils.functional import cached_property

Jeff Widman
  • 22,014
  • 12
  • 72
  • 88
  • Also available in Django `from django.utils.functional import cached_property` – Seb D. Jul 18 '17 at 08:47
  • 1
    I don't know why your answer is not upvoted more. For me, decorating a function with `@functools.lru_cache(1)` before decorating it with `@property` works wonders. – pepoluan Jun 28 '19 at 07:01
  • Just to clarify it for others: "decorating a function with `@functools.lru_cache(1)` before decorating it with `@property`" means that `@functools.lru_cache(1)` goes _after_ `@property` (`@functools.lru_cache(1)` closest to the function). – user118967 Jul 28 '20 at 13:41
5

I've had to look it up, since I had this same question.

The functools package from the standard library will be getting a cached_property decorator as well. Unfortunately, it's only available from Python 3.8 (as of time of this post, it's 3.8a0). The alternative to waiting is to use a custom one, such as this one as mentioned by 0xc0de) or Django's, for now, then switch later:

from django.utils.functional import cached_property
# from functools import cached_property # Only 3.8+ :(
  • 2
    You can stack `@property` and `@functools.lru_cache(maxsize)`, you know. And `@functools.lru_cache` is available since Python 3.2 – pepoluan Jun 28 '19 at 06:57
3

The decorator from Denis Otkidach mentioned in @unutbu's answer was published in O'Reilly's Python Cookbook. Unfortunately O'Reilly doesn't specify any license for code examples – just as informal permission to reuse the code.

If you need a cached property decorator with a liberal license, you can use Ken Seehof's @cached_property from ActiveState code recipes. It's explicitly published under the MIT license.

def cached_property(f):
    """returns a cached property that is calculated by function f"""
    def get(self):
        try:
            return self._property_cache[f]
        except AttributeError:
            self._property_cache = {}
            x = self._property_cache[f] = f(self)
            return x
        except KeyError:
            x = self._property_cache[f] = f(self)
            return x

    return property(get)
akaihola
  • 26,309
  • 7
  • 59
  • 69
1

Note: Adding for the sake of completeness of available options.

No, property is not cached by default. However there are several options to get that behaviour, I would like to add one more to that:

https://github.com/pydanny/cached-property

0xc0de
  • 8,028
  • 5
  • 49
  • 75