Welcome, guest | Sign In | My Account | Store | Cart

You need to cache instances based on what arguments are passed to them.

Python, 29 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MementoMetaclass(type):    
    cache = {}

    def __call__(self, *args):
        print "="*20
        print "ClassObj:", self
        print "Args:", args
        print "="*20
        cached = self.cache.get(args, None)
        if not cached:
            instance = type.__call__(self, *args)
            self.cache.update({args:instance})
            return instance
        return cached
        
class Foo(object):
    __metaclass__ = MementoMetaclass
    template = ''
    def __init__(self, arg1, arg2, arg3):
        self.template = arg1

a = Foo(1,2,3)
b = Foo(2,3,4)
c = Foo(1,2,3)
d = Foo(2,3,4)
e = Foo(5,6,7)
f = Foo(5,6,7)

print id(a), id(b), id(c), id(d), id(e), id(f)

In some situations (maybe with web toolkits) you may need to cache your instances, for example using a web toolkit, once you have an instance of a page you don't need to reinstantiate it every time a user connects.

Using this simple metaclass you will be able to have a cache of instances based on what arguments are passed.

4 comments

Ian Bicking 19 years, 10 months ago  # | flag

Threading. You should be careful with this in a threaded environment. Because of the way it is constructured, two threads may share an instance, but won't necessarily do so. Only with proper locking can you ensure that only one instance will exist for a particular set of parameters.

If you do not want threads to share instances you must instead implement a pool mechanism, where you fetch objects from a pool, or if the pool has no appropriate instance you create the object. The objects themselves should probably be wrappers in that case, so that you can override __del__ so that it returns the instance to the pool just before it is garbage collected. Alternately, you can insist that any threads that fetch an object explicitly return it to the pool when they are done, or any classes implement a __del__ method themselves.

Michele Simionato 19 years, 10 months ago  # | flag

You don't really need a metaclass here ... you could just instantiate your instances with a factory function. It would also be clearer to your users. Unless you want to hide the fact that instances are cached and you want your users use the standard instantiation syntax. If this is your goal (for instance because there is a lot of already written code using the standard calling syntax) then the metaclass solution is justified. But I would not use it if I was writing a new application from scratch.

Jonathan Eunice 11 years, 9 months ago  # | flag

This is a sweet pattern for cached classes. I would recommend, however, using a cache key that also includes the class involved, so that MementoMetaclass can safely be the metaclass for multiple classes simultaneously. I've also taken the liberty of reorganizing the code to improve the ordering and use the try: ... except KeyError: ... idiom currently in vogue.

class MementoMetaclass(type):

    cache = {}

    def __call__(self, *args):
        key = (self, ) + args
        try:
            return self.cache[key]
        except KeyError:
            instance = type.__call__(self, *args)
            self.cache[key] = instance
            return instance
Jonathan Eunice 11 years, 7 months ago  # | flag

After using MementoMetaclass in several projects, I extended it to support keyword arguments. This exposes some possible complications (see comments in code), but in practice has not been a problem.

class MementoMetaclass(type):
    """
    Classes that use this caching metaclass will have their instances
    automatically cached based on instantiation-time arguments (i.e. to __init__).
    Super-useful for not repetitively creating expensive-to-create objects.

    See http://code.activestate.com/recipes/286132-memento-design-pattern-in-python/
    """
    cache = {}

    def __call__(self, *args, **kwargs):
        key = (self, ) + args + tuple(kwargs.items())
        try:
            return self.cache[key]
        except KeyError:
            instance = type.__call__(self, *args, **kwargs)
            self.cache[key] = instance
            return instance

    # NB This metaclass caches on call signature, which can vary if keyword args
    # are used. E.g. `def func(a, b=2)` could be called `func(1)`, `func(1,2)`,
    # `func(a=1)`, `func(1, b=2)`, or `func(a=2, b=2)`--and all resolve to the
    # same logical call. And this is just for two parameters! If there are more
    # than one kwarg, they can be arbitrarily ordered, creating *many* logically
    # identical permuations. Thank Goodness Python doesn't allow kwargs to come
    # before positional args, else there'd be even more ways to make the same
    # call.

    # Net net: If you instantiate an object once, then again with a logically
    # identical call but using a different calling structure/signature, the
    # object won't be created and cached just once--it will be created and
    # cached multiple times. This may degrade performance, and can also create
    # errors, if you're counting on memento to create just one object.

    # In most cases, this isn't an issue, because objects tend to be
    # instanitated with a limited number of parameters, and you can take care
    # that you instantiate them with parallel call signatures. Since this works
    # 99% of the time and has a simple implementation, it's worth the price of
    # this inelegance. For the 1% edge-cases where multiple call signature
    # variations must be conclusively resolved to a unique canonical signature,
    # the `inspect` module could be used (e.g. `inspect.getargvalues()` or
    # `inspect.getcallargs()` to create such a unified key.
Created by Valentino Volonghi on Sat, 12 Jun 2004 (PSF)
Python recipes (4591)
Valentino Volonghi's recipes (6)

Required Modules

  • (none specified)

Other Information and Tasks