Search code examples
pythonpython-class

Method for correct/pythonic way to use @cached_property to set a permanent attribute on a Python class


I am trying to set a permanent attribute on a class to be used by other methods throughout the life of the instance of the class

class Test:
    @cached_property
    def datafile_num_rows(self) -> int:
         return self

    def set_num_rows(self):
         # insert some calculations
         calcs = 2
         self.datafile_num_rows = calcs

Given the above class, the following yields the proper output, but it feels quite odd to me to just return self. What would be the most correct/pythonic way to accomplish this?

In [118]: x = Test()

In [119]: x.set_num_rows()

In [120]: x.datafile_num_rows
Out[120]: 2

Solution

  • @cached_property is supposed to be used for holding expensively computed properties. In the example from the docs you can see the emphasis on "otherwise effectively immutable" by using the underscore.

    Your example isn't ever using @cached_property (apart from creating the functools.cached_property on instance instantiation), since an attribute of the same name already exists, as you're setting it in set_num_rows() before you first access datafile_num_rows. Thus, you luckily never return self as that's really not needed either.

    from the docs:

    The cached_property decorator only runs on lookups and only when an attribute of the same name doesn’t exist.

    The relevant part of functools.py

    val = cache.get(self.attrname, _NOT_FOUND)
    if val is _NOT_FOUND:
        val = self.func(instance)
        try:
            cache[self.attrname] = val
    

    The decorator runs on lookup, when you first access x.datafile_num_rows (This includes tabbing autocomplete if you're in the interactive shell and hasattr()). After that initial lookup, you could delete the attribute to have the function run again in which case you'll immediately get the (newly written) attribute again on the next lookup.

    Functools stores your function and runs it exactly once on lookup, stores the result as an attribute and henceforth you access that attribute. That's the "normal" use case. Another way to understand the implications of that would be to consider:

    def abc():
        x = some_heavy_calculation()
        return x
    temp = abc # we create an alias to the function
    abc = abc() # we store the result of the function to a var of its name
    # now abc is not an alias for the function anymore but our x
    
    # we don't want x anymore, not sure why
    abc = temp
    abc = abc() # ...
    

    Considering this switcheroo, I would avoid interacting with anything inside the decorated code from anywhere else within the class, apart from providing attributes it can use or helper methods for its expensive calculations or triggering the lookup exactly when we want it to happen.

    In my understanding the pythonic way to use it taken your example would be:

    class Test:
    
        # I would feel tempted to initialize _calcs in __init__
        # to avoid handling obscure behaviour if someone looks the attribute
        # up before everything is set for it. 
    
        @cached_property
        def datafile_num_rows(self):
             # expensive calculations here
             return expensive_computation(self._calcs)
    
        def set_num_rows(self):
             self._calcs = 2
                 
    

    Just make sure datafile_num_rows isn't looked-up before set_num_rows() is run or handle such cases if that is what you want to provide (including to delete the attribute in order to re-run datafile_num_rows as a function).