| 1 | #!/usr/bin/env python |
|---|
| 2 | # |
|---|
| 3 | # Author: Mike McKerns (mmckerns @caltech and @uqfoundation) |
|---|
| 4 | # Copyright (c) 2012-2016 California Institute of Technology. |
|---|
| 5 | # License: 3-clause BSD. The full license text is available at: |
|---|
| 6 | # - http://mmckerns.github.io/project/mystic/browser/mystic/LICENSE |
|---|
| 7 | """ |
|---|
| 8 | decorators that cache results to memory, to file, or to a database |
|---|
| 9 | """ |
|---|
| 10 | from functools import update_wrapper, wraps as functools_wraps |
|---|
| 11 | from klepto.rounding import deep_round, simple_round |
|---|
| 12 | from klepto.archives import cache as archive_dict |
|---|
| 13 | from klepto.keymaps import stringmap |
|---|
| 14 | from klepto import inf_cache |
|---|
| 15 | from klepto.tools import CacheInfo |
|---|
| 16 | |
|---|
| 17 | __all__ = ['memoize', 'memoized'] |
|---|
| 18 | |
|---|
| 19 | #XXX: use cache maxsize algorithms... where dump to archive if maxsize |
|---|
| 20 | #XXX: memoized fails when decorating a class method ??? |
|---|
| 21 | |
|---|
| 22 | # backward compatibility |
|---|
| 23 | memoized = inf_cache |
|---|
| 24 | |
|---|
| 25 | class memoize(object): |
|---|
| 26 | """This is safecache.inf_cache implemented as a class-based decorator. |
|---|
| 27 | |
|---|
| 28 | Decorator that memoizes a function's return value each time it is called. |
|---|
| 29 | If called later with the same arguments, the memoized value is returned, and |
|---|
| 30 | not re-evaluated. This may lead to memory issues, as cache is not cleared. |
|---|
| 31 | Can memoize a *method* on an object. |
|---|
| 32 | """ |
|---|
| 33 | def __init__(self, cache=None, keymap=None, tol=None, deep=False): |
|---|
| 34 | self.__maxsize = None |
|---|
| 35 | if keymap is None: keymap = stringmap(flat=False) |
|---|
| 36 | if cache is None: cache = archive_dict() |
|---|
| 37 | elif type(cache) is dict: cache = archive_dict(cache) |
|---|
| 38 | self.__cache = cache |
|---|
| 39 | self.__keymap = keymap |
|---|
| 40 | |
|---|
| 41 | if deep: rounded = deep_round |
|---|
| 42 | else: rounded = simple_round |
|---|
| 43 | |
|---|
| 44 | @rounded(tol) |
|---|
| 45 | def rounded_args(*args, **kwds): |
|---|
| 46 | return (args, kwds) |
|---|
| 47 | self.__rounded_args = rounded_args |
|---|
| 48 | return |
|---|
| 49 | |
|---|
| 50 | def __call__(self, user_function): # i.e. 'decorating_function' |
|---|
| 51 | stats = [0, 0, 0] # make statistics updateable non-locally |
|---|
| 52 | HIT, MISS, LOAD = 0, 1, 2 # names for the stats fields |
|---|
| 53 | |
|---|
| 54 | def wrapper(*args, **kwds): |
|---|
| 55 | try: |
|---|
| 56 | _args, _kwds = self.__rounded_args(*args, **kwds) |
|---|
| 57 | key = self.__keymap(*_args, **_kwds) |
|---|
| 58 | except: #TypeError |
|---|
| 59 | result = user_function(*args, **kwds) |
|---|
| 60 | return result |
|---|
| 61 | |
|---|
| 62 | try: |
|---|
| 63 | # get cache entry |
|---|
| 64 | result = self.__cache[key] |
|---|
| 65 | stats[HIT] += 1 |
|---|
| 66 | except KeyError: |
|---|
| 67 | # if not in cache, look in archive |
|---|
| 68 | if self.__cache.archived(): |
|---|
| 69 | self.__cache.load(key) |
|---|
| 70 | try: |
|---|
| 71 | result = self.__cache[key] |
|---|
| 72 | stats[LOAD] += 1 |
|---|
| 73 | except KeyError: |
|---|
| 74 | # if not found, then compute |
|---|
| 75 | result = user_function(*args, **kwds) |
|---|
| 76 | self.__cache[key] = result |
|---|
| 77 | stats[MISS] += 1 |
|---|
| 78 | return result |
|---|
| 79 | |
|---|
| 80 | def archive(obj): |
|---|
| 81 | """Replace the cache archive""" |
|---|
| 82 | self.__cache.archive = obj |
|---|
| 83 | |
|---|
| 84 | def __get_cache(): |
|---|
| 85 | """Get the cache""" |
|---|
| 86 | return self.__cache |
|---|
| 87 | |
|---|
| 88 | def clear(keepstats=False): |
|---|
| 89 | """Clear the cache and statistics""" |
|---|
| 90 | self.__cache.clear() |
|---|
| 91 | if not keepstats: stats[:] = [0, 0, 0] |
|---|
| 92 | |
|---|
| 93 | def info(): |
|---|
| 94 | """Report cache statistics""" |
|---|
| 95 | return CacheInfo(stats[HIT], stats[MISS], stats[LOAD], self.__maxsize, len(self.__cache)) |
|---|
| 96 | |
|---|
| 97 | # interface |
|---|
| 98 | wrapper.__wrapped__ = user_function |
|---|
| 99 | wrapper.info = info |
|---|
| 100 | wrapper.clear = clear |
|---|
| 101 | wrapper.load = self.__cache.load |
|---|
| 102 | wrapper.dump = self.__cache.dump |
|---|
| 103 | wrapper.archive = archive |
|---|
| 104 | wrapper.archived = self.__cache.archived |
|---|
| 105 | wrapper.__cache__ = __get_cache |
|---|
| 106 | return update_wrapper(wrapper, user_function) |
|---|
| 107 | |
|---|
| 108 | # def __repr__(self): |
|---|
| 109 | # """Return the function's docstring.""" |
|---|
| 110 | # return self.func.__doc__ |
|---|
| 111 | |
|---|
| 112 | def __get__(self, obj, objtype): |
|---|
| 113 | """Support instance methods.""" |
|---|
| 114 | import functools |
|---|
| 115 | return functools.partial(self.__call__, obj) |
|---|
| 116 | |
|---|
| 117 | # EOF |
|---|