#!/usr/bin/env python # # memoize.py - Memoization decorators. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # """This module provides a handful of decorators which may be used to memoize a function: .. autosummary:: :nosignatures: memoize Memoize Instanceify memoizeMD5 skipUnchanged """ import logging import hashlib import functools import six log = logging.getLogger(__name__) def memoize(func=None): """Memoize the given function by the value of the input arguments. This function simply returns a :class:`Memoize` instance. """ return Memoize(func) class Memoize(object): """Decorator which can be used to memoize a function or method. Use like so:: @memoize def myfunc(*a, **kwa): ... @memoize() def otherfunc(*a, **kwax): ... A ``Memoize`` instance maintains a cache which contains ``{args : value}`` mappings, where ``args`` are the input arguments to the function, and ``value`` is the value that the function returned for those arguments. When a memoized function is called with arguments that are present in the cache, the cached values are returned, and the function itself is not called. The :meth:`invalidate` method may be used to clear the internal cache. Note that the arguments used for memoization must be hashable, as they are used as keys in a dictionary. """ def __init__(self, *args, **kwargs): """Create a ``Memoize`` object. """ self.__cache = {} self.__func = None self.__defaultKey = '_memoize_noargs_' self.__setFunction(*args, **kwargs) def invalidate(self, *args, **kwargs): """Clears the internal cache. If no arguments are given, the entire cache is cleared. Otherwise, only the cached value for the provided arguments is cleared. """ if len(args) + len(kwargs) == 0: self.__cache = {} else: key = self.__makeKey(*args, **kwargs) try: self.__cache.pop(key) except KeyError: pass def __setFunction(self, *args, **kwargs): """Used internally to set the memoized function. """ if self.__func is not None: return False # A no-brackets style # decorator was used isfunc = (len(kwargs) == 0 and len(args) == 1 and callable(args[0])) if isfunc: self.__func = args[0] return isfunc def __makeKey(self, *a, **kwa): """Constructs a key for use with the cache from the given arguments. """ key = [] if a is not None: key += list(a) if kwa is not None: key += [kwa[k] for k in sorted(kwa.keys())] # This decorator was created without # any arguments specified - use the # default cache key. if len(key) == 0: key = [self.__defaultKey] return tuple(key) def __call__(self, *a, **kwa): """Checks the cache against the given arguments. If a cached value is present, it is returned. Otherwise the memoized function is called, and its value is cached and returned. """ if self.__setFunction(*a, **kwa): return self key = self.__makeKey(*a, **kwa) try: result = self.__cache[key] log.debug(u'Retrieved from cache[{}]: {}'.format(key, result)) except KeyError: result = self.__func(*a, **kwa) self.__cache[key] = result log.debug(u'Adding to cache[{}]: {}'.format(key, result)) return result def memoizeMD5(func): """Memoize the given function. Whenever the function is called, an md5 digest of its arguments is calculated - if the digest has been previously cached, the previous value calculated by the function is returned. """ cache = {} def wrapper(*args, **kwargs): args = list(args) + list(kwargs.values()) hashobj = hashlib.md5() # Convert each arg to a string # representation, then encode # it into a sequence of (utf-8 # compatible) bytes , and take # the hash of those bytes. for arg in args: if not isinstance(arg, six.string_types): arg = str(arg) arg = arg.encode('utf-8') hashobj.update(arg) digest = hashobj.hexdigest() cached = cache.get(digest) if cached is not None: return cached result = func(*args, **kwargs) log.debug(u'Adding to MD5 cache[{}]: {}'.format( digest, result)) cache[digest] = result return result return wrapper def skipUnchanged(func): """This decorator is intended for use with *setter* functions - a function which accepts a name and a value, and is intended to set some named attribute to the given value. This decorator keeps a cache of name-value pairs. When the decorator is called with a specific name and value, the cache is checked and, if the given value is the same as the cached value, the decorated function is *not* called. If the given value is different from the cached value (or there is no value), the decorated function is called. .. note:: This decorator ignores the return value of the decorated function. :returns: ``True`` if the underlying setter function was called, ``False`` otherwise. """ import numpy as np cache = {} def wrapper(name, value, *args, **kwargs): oldVal = cache.get(name, None) if oldVal is not None: oldIsArray = isinstance(oldVal, np.ndarray) newIsArray = isinstance(value, np.ndarray) isarray = oldIsArray or newIsArray if isarray: a = np.array(oldVal, copy=False) b = np.array(value, copy=False) nochange = (a.shape == b.shape) and np.allclose(a, b) else: nochange = oldVal == value if nochange: return False func(name, value, *args, **kwargs) cache[name] = value return True return wrapper class Instanceify(object): """This class is intended to be used to decorate other decorators, so they can be applied to instance methods. For example, say we have the following class:: class Container(object): def __init__(self): self.__items = {} @skipUnchanged def set(self, name, value): self.__items[name] = value Given this definition, a single :func:`skipUnchanged` decorator will be created and shared amongst all ``Container`` instances. This is not ideal, as the value cache created by the :func:`skipUnchanged` decorator should be associated with a single ``Container`` instance. By redefining the ``Container`` class definition like so:: class Container(object): def __init__(self): self.__items = {} @Instanceify(skipUnchanged) def set(self, name, value): self.__items[name] = value a separate :func:`skipUnchanged` decorator is created for, and associated with, every ``Container`` instance. This is achieved because an ``Instanceify`` instance is a descriptor. When first accessed as an instance attribute, an ``Instanceify`` instance will create the real decorator function, and replace itself on the instance. """ def __init__(self, realDecorator): """Create an ``Instanceify`` decorator. :arg realDecorator: A reference to the decorator that is to be *instance-ified*. """ self.__realDecorator = realDecorator self.__func = None def __call__(self, func): """Called immediately after :meth:`__init__`, and passed the method that is to be decorated. """ self.__func = func return self def __get__(self, instance, cls): """When an ``Instanceify`` instance is accessed as an attribute of another object, it will create the real (instance-ified) decorator, and replace itself on the instance with the real decorator. """ if instance is None: return self.__func method = functools.partial(self.__func, instance) decMethod = self.__realDecorator(method) setattr(instance, self.__func.__name__, decMethod) return functools.update_wrapper(decMethod, self.__func)