Search code examples
pythoncachingpropertiesnamedtuplepython-descriptors

Using a cached property on a named tuple


from typing import NamedTuple
from functools import cached_property

class Rectangle(NamedTuple):
    x: int
    y: int

    @cached_property
    def area(self):
        return self.x * self.y

I thought this class definition would complain something about the __slots__ on Rectangle, but apparently the class definition is valid. It doesn't fail until too late, if/when the getter is actually accessed:

>>> rect = Rectangle(2, 3)
>>> rect.area
...
TypeError: Cannot use cached_property instance without calling __set_name__ on it.
>>> Rectangle.

Well, that's weird, but okay..

>>> Rectangle.area.__set_name__(Rectangle, "area")
>>> rect.area
...
TypeError: No '__dict__' attribute on 'Rectangle' instance to cache 'area' property.

Is there a better recipe for cached properties on named tuples? Requirements:

  • It should not appear to be a real field (x, y, area = rect should not be possible)
  • It should be lazy (not eagerly computed) and cached (not recomputed every time accessed)
  • Wherever the storage is should not leak memory (it should be deleted when the tuple instance itself is deleted)

Solution

  • You probably want a cached_property on a dataclass with the frozen=True setting instead. The frozen setting allows the dataclass to function like an immutable NamedTuple:

    from dataclasses import dataclass
    from functools import cached_property
    
    @dataclass(frozen=True)
    class Rectangle:
        x: int
        y: int
        
        @cached_property
        def area(self):
            print("Fresh compute of area")
            return self.x * self.y
    
    
    r = Rectangle(2, 4)
    
    r.area
    Fresh compute of area
    8
    
    # this is cached, so print doesn't run on a second
    # call to area
    r.area
    8
    
    # can't add new attributes
    r.z = 'thing'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 4, in __setattr__
    dataclasses.FrozenInstanceError: cannot assign to field 'z'
    
    # can't reassign existing attributes
    r.x = 4
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<string>", line 4, in __setattr__
    dataclasses.FrozenInstanceError: cannot assign to field 'x'
    
    # You get __str__ for free
    print(r)
    Rectangle(x=2, y=4)
    
    # as well as __hash__
    hash(r)
    3516302870623680066
    
    # and __eq__
    r == Rectangle(2, 4)
    True
    
    r == Rectangle(1, 4)
    False