2

I have a memory heavy class, say a type representing a high-resolution resource (ie: media, models, data, etc), that can be instantiated multiple times with identical parameters, such as same filename of the resource loaded multiple times.

I'd like to implement some sort of unbounded caching on object creation to memory reuse identical instances if they have the same constructor parameter values. I don't care about mutability of one instance affecting the other shared ones. What is the easiest pythonic way to achieve this?

Note that neither singletons, object-pools, factory methods or field properties meet my use case.

eliangius
  • 326
  • 3
  • 10

1 Answers1

3

You could use a factory function with functools.cache:

import functools

@functools.cache
def make_myclass(*args, **kwargs):
    return MyClass(*args, **kwargs)

EDIT: Apparently you can decorate your class directly to get the same effect:

@functools.cache
class Foo:
    def __init__(self, a):
        print("Creating new instance")
        self.a = a

>>> Foo(1)
Creating new instance
<__main__.Foo object at 0x0000021D7D61FFA0>
>>> Foo(1)
<__main__.Foo object at 0x0000021D7D61FFA0>
>>> Foo(2)
Creating new instance
<__main__.Foo object at 0x0000021D7D61F250> 

Note the same memory address both times Foo(1) is called.

Edit 2: After some playing around, you can get your default-respecting instance cache behavior if you override __new__ and do all of your caching and instantiation there:

class Foo:
    _cached = {}
    
    def __new__(cls, a, b=3):
        attrs = a, b
        if attrs in cls._cached:
            return cls._cached[attrs]
            
        print(f"Creating new instance Foo({a}, {b})")

        new_foo = super().__new__(cls)
        new_foo.a = a
        new_foo.b = b
        cls._cached[attrs] = new_foo  
        return new_foo
     
a = Foo(1)
b = Foo(1, 3)
c = Foo(b=3, a=1)
d = Foo(4)

print(a is b)
print(b is c)
print(c is d)

output:

Creating new instance Foo(1, 3)
Creating new instance Foo(4, 3)
True
True
False

The __init__ will still be called after __new__, so you will want to do your expensive initialization (or all of it) in __new__ after the cache check.

jprebys
  • 2,469
  • 1
  • 11
  • 16
  • I constrained the question a bit, but otherwise this does it neatly! Is there a way to cache the class or constructor? or invoke the factory within the class creation? Cannot use public factory methods as their usage cannot be enforced in the call sites. – eliangius Feb 14 '23 at 22:14
  • 1
    Updated with class decorator solution – jprebys Feb 14 '23 at 22:25
  • Nice! Would you know how make this solution give the same instance when the class declares default arguments in the constructor such that `id(Foo()) == id(Foo(a=default))`? – eliangius Feb 14 '23 at 22:42
  • That's a good question, this solution doesn't work with default args. Not sure about that right now, but I can mess around with it! – jprebys Feb 15 '23 at 00:34
  • Updated again with solution for default arguments. – jprebys Feb 15 '23 at 19:10