3
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)

2 Answers 2

2

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
Sign up to request clarification or add additional context in comments.

Comments

1

Try namedtuple instead. This code works as expected:

from collections import namedtuple
from functools import cached_property

class Rectangle(namedtuple('_Rectangle','x y')):
    @cached_property
    def area(self):
        return self.x * self.y

It is a bit weird as the documentation on NamedTuple says that:

class typing.NamedTuple

Typed version of namedtuple. Usage:
class Employee(NamedTuple):
name: str
id: int

This is equivalent to:
Employee = collections.namedtuple('Employee', ['name', 'id'])

Yet, that's what we have.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.