DarkSheep
DarkSheep

Reputation: 125

Does the @properties decorator cache results?

My IDE has "corrected" my code to convert a function (and other code) to a property. I am worried that it might be inefficient.

@property
def output_all_children(self):
    lh = ListHolder()
    traverse_directories(self.start_directory, lh)
    return lh.internal_list

this does some heavy I/O lifting and takes some time. I am wondering now if this is incorrect due to efficiency reasons. I am wondering if the results aren't cached like I hoping would happen.

If this property is accessed several times is it going to rebuild and return the lh.internal_list every time? I would correct this by having a class level variable and updating it when self.start_directory is changed.

I have looked at this: How to create decorator for lazy initialization of a property and it refers to a read-only property while mine would be a updated property

Please no comments on trusting IDEs blindly. I know and that thinking prompted this question.

Upvotes: 2

Views: 707

Answers (2)

kindall
kindall

Reputation: 184191

Indeed, the property will be called each time. This isn't a good use of a property; callers will expect it to take not much longer than an attribute access.

There's a lovely decorator in Pyramid called reify that calls the function the first time, then sets the value as a same-named attribute on the object so that the previously-calculated value is used the next time.

If you don't want to use Pyramid just for the one decorator, you can use this version I wrote (it works like Pyramid's but I wrote it independently):

import functools

def reify(func):

    class Descriptor(object):
        def __get__(self, inst, type=None):
            val = func(inst)
            setattr(inst, func.__name__, val)
            return val

    return functools.wraps(func)(Descriptor())

To make it work nicely with your use case, just del the attribute off the instance when you need it to be recalculated:

@reify
def all_children(self):
    lh = ListHolder()
    traverse_directories(self.start_directory, lh)
    return lh.internal_list

# for internal use only, call when a cached all_children may no longer be valid
def _invalidate_all_children(self):
    try:
        del self.all_children
    except AttributeError:
        pass

Upvotes: 1

Martijn Pieters
Martijn Pieters

Reputation: 1122082

No, a property merely translates attribute access into a function call. No automatic caching takes place.

In other words, the syntax instance.output_all_children is translated into instance.output_all_children() for you.

You can easily add some caching to a property method, or you can reuse the code you link to in your question; merely replace the __set__ method to handle attribute assignment, if you so desire.

Adding some caching:

_output_all_children = None

@property
def output_all_children(self):
    if self._output_all_children is None:
        lh = ListHolder()
        traverse_directories(self.start_directory, lh)
        self._output_all_children = lh.internal_list
    return self._output_all_children

Upvotes: 3

Related Questions