Reputation: 24237
I'm writing a class in python and I have an attribute that will take a relatively long time to compute, so I only want to do it once. Also, it will not be needed by every instance of the class, so I don't want to do it by default in __init__
.
I'm new to Python, but not to programming. I can come up with a way to do this pretty easily, but I've found over and over again that the 'Pythonic' way of doing something is often much simpler than what I come up with using my experience in other languages.
Is there a 'right' way to do this in Python?
Upvotes: 143
Views: 93671
Reputation: 309
As mentioned, functools.cached_property
will work for cached instance attributes. For cached class attributes:
3.9 <= python < 3.13
from functools import cache
class MyClass:
@classmethod
@property
@cache # or lru_cache() for python < 3.9
def foo(cls):
print('expensive calculation')
return 42
>>> MyClass.foo
expensive calculation
42
>>> MyClass.foo
42
And if you want a reusable decorator:
def cached_class_attr(f):
return classmethod(property(cache(f)))
class MyClass:
@cached_class_attr
def foo(cls):
...
python >= 3.13
In 3.13 chaining classmethod
and property
is disallowed so you will have to use metaclasses or a custom decorator, here's an example of a read-only cached attribute:
class MyMeta(type):
@property
@cache
def foo(self):
...
class MyClass(metaclass=MyMeta):
...
MyClass.foo # read-only access
Or alternatively a custom decorator:
class classproperty:
def __init__(self, func) -> None:
functools.update_wrapper(self, func)
def __get__(self, instance, owner):
return self.__wrapped__(owner)
class MyClass:
@classproperty
@cache
def foo(cls):
...
Upvotes: 24
Reputation: 10021
3.8 ≤ Python
@property
and @functools.lru_cache
have been combined into @cached_property
.
import functools
class MyClass:
@functools.cached_property
def foo(self):
print("long calculation here")
return 21 * 2
3.2 ≤ Python < 3.8
You should use both @property
and @functools.lru_cache
decorators:
import functools
class MyClass:
@property
@functools.lru_cache()
def foo(self):
print("long calculation here")
return 21 * 2
This answer has more detailed examples and also mentions a backport for previous Python versions.
Python < 3.2
The Python wiki has a cached property decorator (MIT licensed) that can be used like this:
import random
# the class containing the property must be a new-style class
class MyClass(object):
# create property whose value is cached for ten minutes
@cached_property(ttl=600)
def randint(self):
# will only be evaluated every 10 min. at maximum.
return random.randint(0, 100)
Or any implementation mentioned in the others answers that fits your needs.
Or the above mentioned backport.
Upvotes: 200
Reputation: 16157
Most if not all current answers are about caching instance attributes. To cache class attributes, you can simply use a dictionary. This ensures the attributes are calculated once per class, instead of once per instance.
mapping = {}
class A:
def __init__(self):
if self.__class__.__name__ not in mapping:
print('Expansive calculation')
mapping[self.__class__.__name__] = self.__class__.__name__
self.cached = mapping[self.__class__.__name__]
To illustrate,
foo = A()
bar = A()
print(foo.cached, bar.cached)
gives
Expansive calculation
A A
Upvotes: 1
Reputation: 11
With Python 2, but not Python 3, here's what I do. This is about as efficient as you can get:
class X:
@property
def foo(self):
r = 33
self.foo = r
return r
Explanation: Basically, I'm just overloading a property method with the computed value. So after the first time you access the property (for that instance), foo
ceases to be a property and becomes an instance attribute. The advantage of this approach is that a cache hit is as cheap as possible because self.__dict__
is being used as the cache, and there is no instance overhead if the property is not used.
This approach doesn't work with Python 3.
Upvotes: -4
Reputation: 63536
The dickens
package (not mine) offers cachedproperty
, classproperty
and cachedclassproperty
decorators.
To cache a class property:
from descriptors import cachedclassproperty
class MyClass:
@cachedclassproperty
def approx_pi(cls):
return 22 / 7
Upvotes: 5
Reputation: 10882
Python 3.8 includes the functools.cached_property
decorator.
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.
This example is straight from the docs:
from functools import cached_property
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)
The limitation being that the object with the property to be cached must have a __dict__
attribute that is a mutable mapping, ruling out classes with __slots__
unless __dict__
is defined in __slots__
.
Upvotes: 23
Reputation: 17295
I used to do this how gnibbler suggested, but I eventually got tired of the little housekeeping steps.
So I built my own descriptor:
class cached_property(object):
"""
Descriptor (non-data) for building an attribute on-demand on first use.
"""
def __init__(self, factory):
"""
<factory> is called such: factory(instance) to build the attribute.
"""
self._attr_name = factory.__name__
self._factory = factory
def __get__(self, instance, owner):
# Build the attribute.
attr = self._factory(instance)
# Cache the value; hide ourselves.
setattr(instance, self._attr_name, attr)
return attr
Here's how you'd use it:
class Spam(object):
@cached_property
def eggs(self):
print 'long calculation here'
return 6*2
s = Spam()
s.eggs # Calculates the value.
s.eggs # Uses cached value.
Upvotes: 61
Reputation: 70089
class MemoizeTest:
_cache = {}
def __init__(self, a):
if a in MemoizeTest._cache:
self.a = MemoizeTest._cache[a]
else:
self.a = a**5000
MemoizeTest._cache.update({a:self.a})
Upvotes: 2
Reputation: 15370
You could try looking into memoization. The way it works is that if you pass in a function the same arguments, it will return the cached result. You can find more information on implementing it in python here.
Also, depending on how your code is set up (you say that it is not needed by all instances) you could try to use some sort of flyweight pattern, or lazy-loading.
Upvotes: 1
Reputation: 304503
The usual way would be to make the attribute a property and store the value the first time it is calculated
import time
class Foo(object):
def __init__(self):
self._bar = None
@property
def bar(self):
if self._bar is None:
print "starting long calculation"
time.sleep(5)
self._bar = 2*2
print "finished long caclulation"
return self._bar
foo=Foo()
print "Accessing foo.bar"
print foo.bar
print "Accessing foo.bar"
print foo.bar
Upvotes: 43
Reputation: 1124
The most simple way of doing this would probably be to just write a method (instead of using an attribute) that wraps around the attribute (getter method). On the first call, this methods calculates, saves and returns the value; later it just returns the saved value.
Upvotes: -3