Memoizing decorator

Daishi Harada daishi at gmail.com
Wed Dec 7 20:37:02 EST 2005


Hi,

Sorry about the previous dup; I'm posting through
Google Groups which seems to have burped.

Anyways, I've "improved" things (or at least
got things passing more tests). I now bind
the cache to each object instance (and class
for classmethods).

At least one issue still remains, mostly due
to the fact that I still don't really grok how
descriptors are supposed to work.
I managed to get the

@memoize
@classmethod

case to not break by "delegating" to the
classmethod's __get__ from my memoize's
in order to obtain the "correct" underlying
method/function. It seems, however, that
the classmethod implementation doesn't
do the same delegation, so now the flip
case of

@classmethod
@memoize

is having issues where AFAICT there's no
way for the memoize to know what class
it's bound to because classmethod doesn't
invoke it as a decorator. Therefore in that
configuration the memoize is still using
the cache bound to the underlying function
instead of the class object as would be
natural.

Any thoughts/comments would still be
appreciated.

Thanks,
Daishi


---
# All my debugging prints still remain ...
import types
import sys

class DescriptorMemoize(object):
    def __init__(self, fn):
        print '__init__', self, fn
        self.fn = fn
        self._cache = {}
    def __call__(self, *args):
        # Regular invocation case
        print '__call__', self, args
        return self._callmain(self.fn, self._cache, args)
    def _callmain(self, fn, cache, args):
        # The basic logic here is the same as in the
        # wiki decorator library.
        print '_callmain', fn, cache, args
        try:
            return cache[args]
        except KeyError:
            value = fn(*args)
            cache[args] = value
            return value
        except TypeError:
            return fn(*args)
    def __get__(self, inst, owner):
        # Method invocation case
        print '__get__', self, inst, owner
        # Get the right underlying method
        try:
            fn = self.fn.__get__(inst, owner)
            print 'got bound'
        except AttributeError:
            fn = self.fn
            print 'using default fn'
        # Get the right cache
        # (Generally get the instance's cache)
        if (inst is None or
            isinstance(self.fn, classmethod)):
            print 'using owner cache', owner
            obj = owner
        else:
            print 'using inst cache', inst
            obj = inst
        try:
            # Bypass regular attribute lookup
            maincache = obj.__dict__['_memoizecache']
            print 'found maincache in obj', obj
        except KeyError:
            maincache = {}
            obj._memoizecache = maincache
            print 'created new maincache on', obj
        cache = maincache.setdefault(self.fn, {})
        print 'maincache+id , cache', maincache, id(maincache), cache
        def call(*args):
            print 'call', args
            return self._callmain(fn, cache, args)
        return call

def mk_memoize_test_class(memoize_fn):
    class A(object):
        n_insts = 0

        def __init__(self, n):
            self.n = n
            A.n_insts += 1

        # De-decorate so we can test things easier
        def meth(self, x):
            print '(eval-meth %s %s)' % (str(self), str(x))
            return x+self.n
        mem_meth = memoize_fn(meth)

        # The following will memoize the wrong answer
        # unless all instances are created first.
        def clsmeth(cls, x):
            print '(eval-clsmeth %s %s)' % (str(cls), str(x))
            return x+cls.n_insts
        cls_clsmeth = classmethod(clsmeth)
        memcls_clsmeth = memoize_fn(cls_clsmeth)
        clsmem_clsmeth = classmethod(memoize_fn(clsmeth))

    return A

def test_memoize(memoize_fn):
    def printsep():
        print '-'*30
        sys.stdout.flush()

    @memoize_fn
    def fn(x):
        print '(eval %s)' % str(x)
        return x

    printsep()
    print 'bare fn'
    print fn(1)
    print fn(1)
    printsep()

    A = mk_memoize_test_class(memoize_fn)
    def test_class(a):
        for f in ('meth', 'mem_meth'):
            print f
            m = getattr(a, f)
            try:
                print m(1)
                print m(1)
            except Exception, e:
                print 'XXX', e
            printsep()
        for (label, o) in (('inst', a), ('cls', A)):
            for f in ('cls_clsmeth', 'memcls_clsmeth',
'clsmem_clsmeth'):
                print label
                print f
                m = getattr(o, f)
                try:
                    print m(1)
                    print m(1)
                except Exception, e:
                    print 'XXX', e
                printsep()

    a1 = A(1)
    a2 = A(10)
    printsep()
    print 'Testing a1'
    printsep()
    test_class(a1)
    printsep()
    print 'Testing a2'
    printsep()
    test_class(a2)

if __name__ == '__main__':
    test_memoize(DescriptorMemoize)




More information about the Python-list mailing list