The following paradigm happens for "sage.combinat.posets.posets.FinitePoset" and their "linear_extensions", but it's such an easy mistake to make that I'd almost consider it a flaw in our design:
class A(Parent,UniqueRepresentation):
....
@cached_method
def BfromA(self):
return B(A)
class B(Parent,UniqueRepresentation):
...
If one does
a = A(....)
b = a.BfromA()
del a,b
one leaks memory. This is because the (global!) UniqueRepresentation cache for B will have a strong reference to a (as a key in a WeakValueDictionary) , since it's a construction parameter for b, and a will be holding a strong reference to b because BfromA was declared a cached method. Hence, a keeps b alive, and as long as b is alive, a will be kept alive due to the strong reference in the weakvaluedict.
The solution here probably is to NOT cache BfromA, since the lookup performed by B(A) will be very fast anyway (if the thing already exists), but you can see how difficult it now becomes to decide whether you can safely stick a @cached_method decorator somewhere: If the result contains a UniqueRepresentation object that is constructed using something linked to the object on which the cached_method lives, you probably can't. But this becomes a highly non-local thing to decide, because now you need to know the implementation details of lots of things you might be using.
The real problem here is UniqueRepresentation. Fundamentally, that breaks locality. We try to hide the problems a little bit by using a WeakValueDict to cache things, but as this example shows, it only takes one extra level to let the problems resurface. Any ideas on how we can mitigate these problems? Educating developers perhaps?