Functools addition: @reslot decorator

My app gives the user loads and loads of lightweight objects to work with, but they have rich APIs that may cache more state in them if required. I’d been using __slots__ to make them lightweight, and caching stuff in one slot or another, writing lots of repetitive @property to that effect. I really wanted to use @cached_property, but that wouldn’t work with __slots__…and then I found a way to make it work.

The reslot package provides the @reslot class decorator. Though it requires the decorated class to have a __slots__ entry named __dict__, it doesn’t put a normal dictionary in there. Instead, it puts a MutableMapping with its own __slots__ generated to match whatever @cached_propertys are declared on the decorated class. This __dict__ will never grow, and has the same performance and memory benefits as __slots__, since it does, in fact, store its data that way.

To my knowledge, I am the only user of @reslot presently. I invite you to try it out and file issues as needed. When @reslot is mature, I think it would make a good addition to functools, since it enhances the functionality of @cached_property.

2 Likes

It does not:

  • You still have an extra object there. Even if very low, this has extra overhead. Infact, the memory overhead is probably higher in ~most cases compared to using a normal __dict__ because of key-sharing dictionaries and the fact that you need to generate a new class. (this is difficult to measure, so unless you have very careful benchmarks I wouldn’t trust them)
  • It’s an extra indirection layer no matter what. This means one more pointer indirection for every access. This isn’t a lot compared to most of the python machinery, but it can add up, and it’s definitely not the same performance benefits.

IMO the ideal long term solution is to have __slots__ be mutable until the type is “materialized” (which already has a semi-precise definition). Then your reslot generator could literally have the same performance benefits.

2 Likes

Did you mean most cases generally, or most cases like this?:

A hundred thousand instances of these objects is pretty normal for me. A few extra class definitions doesn’t seem like a big deal.

Bravo. People have long wanted cached_property to play nicely with __slots__. AFAICT, you’re the first to think of a reasonable way to harmonize the two.

1 Like

attrs supports @cached_property in its slotted classes.

That specifies that attrs rewrites uses of the standard functools.cached_property. So… it’s not functools.cached_property when it gets to running.

If the standard cached_property were to change to support __slots__, that would be fine too, and make @reslot irrelevant; but I think it would require changes to the semantics of __slots__, as @MegaIng says.

Most cases generally, but I am not sure about your specific case. Did you measure total memory usage of the entire process for a few examples? You can’t trust methods to get the memory used by each individual object since those may be unreliable & change when observed.

I guess the question is if you need the effect of a cached property or specific implementation details of the @cached_property decorator from functools.

attrs recreates the class to create the slots for each attribute and then moves the function calls to within a generated __getattr__ function to deal with the name clash between the slot descriptor and the wrapped function. Working the way attrs does is probably the best current option as the cached result is stored directly in the slotted attribute so accessing it should be fast, even if the initial call has extra indirection.

1 Like

That is also an interesting workaround. Pretty cool. Thanks for sharing this.

Since cached_property is essentially self-modifying code, do you know if it conflicts with other tooling that expects static code (linters, code coverage, type checkers, or Cython)? Maybe the attrs workaround would work for them as well.