Reputation: 1052
I have the following code for lazily calculating a value in python 3.5. I have also tried the @cached_property
decorator with the same result, so I will use this for simplicity.
class Foo:
@property
def expensive_object(self):
if not hasattr(self, "_expensive_object"):
print("Lengthy initialization routine")
self._expensive_object = 2
return self._expensive_object
The problem is that it gets evaluated when I pass it as an argument to a function, even if it ends up not being used inside, as in this example:
def bar(some_parameter, another_parameter):
if some_parameter != 10:
print(some_parameter)
else:
print(another_parameter)
From the following output we see that it got evaluated just by being passed, but it was not strictly necessary, as the code did not try to use it.
In [23]: foo1 = Foo()
...: bar(3, foo1.expensive_object)
Lengthy initialization routine
3
In [24]: bar(3, foo1.expensive_object)
3
There are of situations in which my script could run without ever needing to evaluate, but it ends up doing it anyway because of such cases.
It would also not be practical to factor out the parameter. I also use it in the __init__
of composed member objects.
If possible I would like to make the property even more lazy in that it should evaluate only when actually read.
Upvotes: 2
Views: 2591
Reputation: 22463
Python lacks simple, idiomatic lazy property evaluation at the level you seek.
There are several schemes for getting lazy property evaluation like that, but they involve the participation and cooperation of the called function (bar
). E.g. you could pass in an object and a property name
def bar2(x, y, propname):
if x != 10:
print(x)
else:
print(getattr(y, propname))
bar2(3, foo1, 'expensive_object')
Or you could pass in a callable like a lambda:
def bar3(x, y):
if x != 10:
print(x)
else:
print(y())
bar3(3, lambda: foo1.expensive_object)
But for all its refinement, Python is a pretty simple language at heart.
It doesn't do a lot of don't-evaluate-it-if-you-don't-need-to
optimizations that even the simplest C or Java compiler
do routinely. It doesn't maintain the almost metaphysical lvalue/rvalue
distinction you see in Perl (which would be really helpful here).
And it doesn't try to dynamically insert and evaluate
thunks to delay property invocation.
When you call foo1.expensive_object
, it's going to compute that value and
hand it over. If you want it done differently you're going to have to make
significant other arrangements, such as above.
If you're going to routinely need delayed / lazy evaluation, it can be convenient to define an evaluate-if-necessary helper function:
def get_value(x):
return x() if hasattr(x, '__call__') else x
That way you can somewhat regularize evaluating y, when needed. The using function still has to cooperate, but this allows you to pass in a static value when you desire, or a lambda when you need to make evaluation lazier:
def bar4(x, y):
if x != 10:
print(x)
else:
print(get_value(y))
bar4(3, 33) # works
bar4(4, lambda: foo1.expensive_object) # also works!
Upvotes: 8