Recently I've been writing a bunch of code like this:
class A:
def __init__(self, x):
self.x = x
self._y = None
def y(self):
if self._y is None:
self._y = big_scary_function(self.x)
return self._y
def z(self, i):
return nice_easy_function(self.y(), i)
In a given class I may have a number of things working like this y
, and I may have other things that use the stored pre-calculated values. Is this the best way to do things or would you recommend something different?
Note that I don't pre-calculate here because you might use an instance of A
without making use of y
.
I've written the sample code in Python, but I'd be interested in answers specific to other languages if relevant. Conversely I'd like to hear from Pythonistas about whether they feel this code is Pythonic or not.
First thing: this is a very common pattern in Python (there's even a
cached_property
descriptor class somewhere - in Django IIRC).This being said there are at least two potential issues here.
The first one is common to all 'cached properties' implementations and is the fact that one usually doesn't expect an attribute access to trigger some heavy computation. Whether it's really an issue depends on the context (and near-religious opinions of the reader...)
The second issue - more specific to your example - is the traditional cache invalidation / state consistency problem: Here you have
y
as a function ofx
- or at least that's what one would expect - but rebindingx
will not updatey
accordingly. This can be easily solved in this case by makingx
a property too and invalidating_y
on the setter, but then you have even more unexpected heavy computation happening.In this case (and depending on the context and computation cost) I'd probably keep memoization (with invalidation) but provide a more explicit getter to make clear we might have some computation going on.
Edit: I misread your code and imagined a property decorator on
y
- which shows how common this pattern is ;). But my remarks still make sense specially when a "self proclaimed pythonista" posts an answer in favour of a computed attribute.Edit: if you want a more or less generic "cached property with cache invalidation", here's a possible implementation (might need more testing etc):
Not that I think the added cognitive overhead is worth the price actually - most often than not a plain inline implementation makes the code easier to understand and maintain (and doesn't require much more LOCs) - but it still might be useful if a package requires a lot of cached properties and cache invalidation.