Changeset 665 for branches


Ignore:
Timestamp:
05/28/13 16:05:22 (3 years ago)
Author:
mmckerns
Message:

added 'memoize' features (archiving, rounding, etc) to caches in cache.py;
chopped up memoize to archives and rounding; added 'load' to CacheInfo?

Location:
branches/decorate
Files:
5 edited
2 copied

Legend:

Unmodified
Added
Removed
  • branches/decorate/archives.py

    r664 r665  
    11""" 
    2 decorators that cache results to memory, to file, or to a database 
     2custom caching dict, which archives results to memory, file, or database 
    33""" 
    4 from keymaps import stringmap 
    5  
    6 __all__ = ['memoize','memoized','archive_dict',\ 
    7            'null_archive','file_archive','db_archive'] 
    8  
    9 def isiterable(x): 
    10  #try: 
    11  #    from collections import Iterable 
    12  #    return isinstance(x, Iterable) 
    13  #except ImportError: 
    14   try: 
    15     iter(x) 
    16     return True 
    17   except TypeError: return False 
    18  #return hasattr(x, '__len__') or hasattr(x, '__iter__') 
    19  
    20  
    21 def deep_round_factory(tol): 
    22   def deep_round(*args, **kwds): 
    23     argstype = type(args)  
    24     _args = list(args) 
    25     _kwds = kwds.copy() 
    26     for i,j in enumerate(args): 
    27       if isinstance(j, float): _args[i] = round(j, tol) # don't round int 
    28       elif isinstance(j, (str, unicode, type(BaseException()))): continue 
    29       elif isinstance(j, dict): _args[i] = deep_round(**j)[1] 
    30       elif isiterable(j): #XXX: fails on the above, so don't iterate them 
    31         jtype = type(j) 
    32         _args[i] = jtype(deep_round(*j)[0]) 
    33     for i,j in kwds.items(): 
    34       if isinstance(j, float): _kwds[i] = round(j, tol) 
    35       elif isinstance(j, (str, unicode, type(BaseException()))): continue 
    36       elif isinstance(j, dict): _kwds[i] = deep_round(**j)[1] 
    37       elif isiterable(j): #XXX: fails on the above, so don't iterate them 
    38         jtype = type(j) 
    39         _kwds[i] = jtype(deep_round(*j)[0]) 
    40     return argstype(_args), _kwds 
    41   return deep_round 
    42  
    43 """ 
    44 >>> deep_round = deep_round_factory(tol=0)  #FIXME: convert to decorator !!! 
    45 >>> deep_round([1.12,2,{'x':1.23, 'y':[4.56,5.67]}], x=set([11.22,44,'hi'])) 
    46 (([1.0, 2, {'y': [5.0, 6.0], 'x': 1.0}],), {'x': set([11.0, 'hi', 44])}) 
    47 """ 
    48  
    49 def deep_round(tol=0): 
    50   def dec(f): 
    51     def func(*args, **kwds): 
    52       if tol is None: 
    53         _args,_kwds = args,kwds 
    54       else: 
    55         _deep_round = deep_round_factory(tol) 
    56         _args,_kwds = _deep_round(*args, **kwds) 
    57       return f(*_args, **_kwds) 
    58     return func 
    59   return dec 
    60  
    61  
    62 def simple_round_factory(tol): 
    63   def simple_round(*args, **kwds): 
    64     argstype = type(args)  
    65     _args = list(args) 
    66     _kwds = kwds.copy() 
    67     for i,j in enumerate(args): 
    68       if isinstance(j, float): _args[i] = round(j, tol) # don't round int 
    69     for i,j in kwds.items(): 
    70       if isinstance(j, float): _kwds[i] = round(j, tol) 
    71     return argstype(_args), _kwds 
    72   return simple_round 
    73  
    74 def simple_round(tol=0): #NOTE: only rounds floats, nothing else 
    75   def dec(f): 
    76     def func(*args, **kwds): 
    77       if tol is None: 
    78         _args,_kwds = args,kwds 
    79       else: 
    80         _simple_round = simple_round_factory(tol) 
    81         _args,_kwds = _simple_round(*args, **kwds) 
    82       return f(*_args, **_kwds) 
    83     return func 
    84   return dec 
    85  
    86  
    87 def shallow_round_factory(tol): 
    88   from numpy import round as around #XXX: try or numpy makes this slow 
    89   def shallow_round(*args, **kwds): 
    90     argstype = type(args)  
    91     _args = list(args) 
    92     _kwds = kwds.copy() 
    93     for i,j in enumerate(args): 
    94       try: 
    95         jtype = type(j) 
    96         _args[i] = jtype(around(j, tol)) 
    97       except: pass 
    98     for i,j in kwds.items(): 
    99       try: 
    100         jtype = type(j) 
    101         _kwds[i] = jtype(around(j, tol)) 
    102       except: pass 
    103     return argstype(_args), _kwds 
    104   return shallow_round 
    105  
    106 def shallow_round(tol=0): #NOTE: rounds floats, lists, arrays one level deep 
    107   def dec(f): 
    108     def func(*args, **kwds): 
    109       if tol is None: 
    110         _args,_kwds = args,kwds 
    111       else: 
    112         _shallow_round = shallow_round_factory(tol) 
    113         _args,_kwds = _shallow_round(*args, **kwds) 
    114       return f(*_args, **_kwds) 
    115     return func 
    116   return dec 
    117  
    118  
    119 #FIXME: the below need expiration of cache due to time, calls, etc... 
    120 #FIXME: memoize*_round fails when decorating a class method 
     4 
     5__all__ = ['archive_dict', 'null_archive', 'file_archive', 'db_archive'] 
    1216 
    1227class archive_dict(dict): 
     
    414299 
    415300 
    416 def memoized(cache=None, keymap=None, tol=None, deep=False): 
    417     """Decorator that memoizes a function's return value each time it is called. 
    418     If called later with the same arguments, the memoized value is returned, and 
    419     not re-evaluated.  This may lead to memory issues, as cache is not cleared. 
    420     This decorator takes an integer tolerance 'tol', equal to the number of 
    421     decimal places to which it will round off floats. 
    422  
    423     cache = storage hashmap (default is {}) 
    424     keymap = cache key encoder (default is keymaps.stringmap(flat=False)) 
    425     tol = integer tolerance for rounding (default is None) 
    426     deep = boolean for rounding depth (default is False, i.e. 'shallow') 
    427     """ 
    428     if keymap is None: keymap = stringmap(flat=False) 
    429     if cache is None: cache = archive_dict() 
    430     elif type(cache) is dict: cache = archive_dict(cache) 
    431     # does archive make sense with database, file, ?... (requires more thought) 
    432  
    433     if deep: rounded = deep_round 
    434     else: rounded = simple_round 
    435    #else: rounded = shallow_round #FIXME: slow 
    436  
    437     @rounded(tol) 
    438     def rounded_args(*args, **kwds): 
    439         return (args, kwds) 
    440  
    441     def dec(f): 
    442         def func(*args, **kwds): 
    443             try: 
    444                 _args, _kwds = rounded_args(*args, **kwds) 
    445                 argstr = keymap(*_args, **_kwds) 
    446                 if cache.has_key(argstr): 
    447                     return cache[argstr] 
    448                 if cache.archived(): 
    449                     cache.load(argstr) 
    450                     if cache.has_key(argstr): 
    451                         return cache[argstr] 
    452                 res = f(*args, **kwds) 
    453                 cache[argstr] = res  #XXX: any automated dump to archive? 
    454                 return res 
    455             except: #TypeError 
    456                 return f(*args, **kwds) 
    457         func.cache = cache 
    458         return func 
    459     return dec 
    460  
    461 #FIXME: use cache maxsize algorithms... where dump to archive if maxsize 
    462  
    463 class memoize(object): 
    464     """Decorator that memoizes a function's return value each time it is called. 
    465     If called later with the same arguments, the memoized value is returned, and 
    466     not re-evaluated.  This may lead to memory issues, as cache is not cleared. 
    467     Can memoize a *method* on an object. 
    468     """ 
    469     def __init__(self, cache=None, keymap=None, tol=None, deep=False): 
    470 #       self.func = func 
    471         if keymap is None: keymap = stringmap(flat=False) 
    472         if cache is None: cache = archive_dict() 
    473         elif type(cache) is dict: cache = archive_dict(cache) 
    474         self.cache = cache 
    475         self.__keymap = keymap 
    476  
    477         if deep: rounded = deep_round 
    478         else: rounded = simple_round 
    479  
    480         @rounded(tol) 
    481         def rounded_args(*args, **kwds): 
    482             return (args, kwds) 
    483         self.__rounded_args = rounded_args 
    484         return 
    485  
    486     def __call__(self, func): 
    487         self.func = func 
    488         import functools 
    489         @functools.wraps(func) 
    490         def dec(*args, **kwds): 
    491             try: 
    492                 _args, _kwds = self.__rounded_args(*args, **kwds) 
    493                 argstr = self.__keymap(*_args, **_kwds) 
    494                 if self.cache.has_key(argstr): 
    495                     return self.cache[argstr] 
    496                 if self.cache.archived(): 
    497                     self.cache.load(argstr) 
    498                     if self.cache.has_key(argstr): 
    499                         return self.cache[argstr] 
    500                 res = self.func(*args, **kwds) 
    501                 self.cache[argstr] = res 
    502                 return res 
    503             except: #TypeError 
    504                 return self.func(*args, **kwds) 
    505         return dec 
    506  
    507     def __repr__(self): 
    508         """Return the function's docstring.""" 
    509         return self.func.__doc__ 
    510  
    511     def __get__(self, obj, objtype): 
    512         """Support instance methods.""" 
    513         import functools 
    514         return functools.partial(self.__call__, obj) 
    515  
    516301# EOF 
  • branches/decorate/cache.py

    r663 r665  
    1212from functools import update_wrapper 
    1313from threading import RLock 
    14 from keymaps import hashmap as _keymap 
    15  
    16 _CacheInfo = namedtuple("CacheInfo", ['hits','misses','maxsize','currsize']) 
     14from rounding import deep_round, simple_round 
     15from archives import archive_dict 
     16from keymaps import hashmap 
     17 
     18_CacheInfo = namedtuple("CacheInfo", ['hit','miss','load','maxsize','size']) 
    1719 
    1820class Counter(dict): 
     
    2123        return 0 
    2224 
     25#XXX: what about caches that expire due to time, calls, etc... 
     26#XXX: check the impact of not serializing by default, and hashmap by default 
     27 
    2328def no_cache(*arg, **kwd): 
    24     '''empty cache decorator. 
    25  
    26     View the cache statistics named tuple (hits, misses, maxsize, currsize) with 
    27     f.info().  Clear the cache and statistics with f.clear().  Access the 
    28     underlying function with f.__wrapped__.  This function does not cache, but 
    29     is a dummy that collects statistics and conforms to the caching interface. 
    30  
     29    '''empty (NO) cache decorator. 
     30 
     31    Unlike other cache decorators, this decorator does not cache.  It is a 
     32    dummy that collects statistics and conforms to the caching interface.  This 
     33    decorator takes an integer tolerance 'tol', equal to the number of decimal 
     34    places to which it will round off floats, and a bool 'deep' for whether the 
     35    rounding on inputs will be 'shallow' or 'deep'. 
     36 
     37    keymap = cache key encoder (default is keymaps.hashmap(flat=True)) 
     38    tol = integer tolerance for rounding (default is None) 
     39    deep = boolean for rounding depth (default is False, i.e. 'shallow') 
     40 
     41    If *keymap* is given, it will replace the hashing algorithm for generating 
     42    cache keys.  Several hashing algorithms are available in 'keymaps'. The 
     43    default keymap requires arguments to the cached function to be hashable. 
     44 
     45    If the keymap retains type information, then arguments of different types 
     46    will be cached separately.  For example, f(3.0) and f(3) will be treated 
     47    as distinct calls with distinct results.  Cache typing has a memory penalty, 
     48    and may also be ignored by some 'keymaps'.  Here, the keymap is only used 
     49    to look up keys in an associated archive. 
     50 
     51    View cache statistics (hit, miss, load, maxsize, size) with f.info(). 
     52    Clear the cache and statistics with f.clear().  Replace the cache archive 
     53    with f.archive(obj).  Load from the archive with f.load(), and dump from 
     54    the cache to the archive with f.dump(). 
    3155    ''' 
    3256    maxsize = 0 
    33     cache = dict() #XXX: tuple, None, ...? 
     57 
     58    keymap = kwd.get('keymap', None) 
     59    if keymap is None: keymap = hashmap(flat=True) #XXX: stringmap ? 
     60    cache = archive_dict() 
     61 
     62    tol = kwd.get('tol', None) 
     63    deep = kwd.get('deep', False) 
     64    if deep: rounded = deep_round 
     65    else: rounded = simple_round 
     66   #else: rounded = shallow_round #FIXME: slow 
     67 
     68    @rounded(tol) 
     69    def rounded_args(*args, **kwds): 
     70        return (args, kwds) 
    3471 
    3572    def decorating_function(user_function): 
    36         stats = [0, 0]                  # make statistics updateable non-locally 
    37         HITS, MISSES = 0, 1             # names for the stats fields 
     73       #cache = dict()                  # mapping of args to results 
     74        stats = [0, 0, 0]               # make statistics updateable non-locally 
     75        HIT, MISS, LOAD = 0, 1, 2       # names for the stats fields 
    3876        _len = len                      # localize the global len() function 
    3977       #lock = RLock()                  # linkedlist updates aren't threadsafe 
    4078 
    4179        def wrapper(*args, **kwds): 
    42             # compute 
    43             result = user_function(*args, **kwds) 
    44             stats[MISSES] += 1 
     80            #XXX: add try/except... any failure has result = f(*args, **kwds) 
     81            _args, _kwds = rounded_args(*args, **kwds) 
     82            key = keymap(*_args, **_kwds) 
     83 
     84            # look in archive 
     85            if cache.archived(): 
     86                cache.load(key) 
     87            try: 
     88                result = cache[key] 
     89                cache.clear() 
     90                stats[LOAD] += 1 
     91            except KeyError: 
     92                # if not found, then compute 
     93                result = user_function(*args, **kwds) 
     94                cache[key] = result 
     95                stats[MISS] += 1 
     96 
     97            # purge cache 
     98            if _len(cache) > maxsize: 
     99                if cache.archived(): 
     100                    cache.dump() 
     101                cache.clear()  
    45102            return result 
    46103 
    47         def clear(): 
     104        def archive(obj): 
     105            """Replace the cache archive""" 
     106            cache.archive = obj 
     107 
     108        def clear(keepstats=False): 
    48109            """Clear the cache and statistics""" 
    49             stats[:] = [0, 0] 
     110            if not keepstats: stats[:] = [0, 0, 0] 
    50111 
    51112        def info(): 
    52113            """Report cache statistics""" 
    53             return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) 
     114            return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 
    54115 
    55116        # interface 
     
    57118        wrapper.info = info 
    58119        wrapper.clear = clear 
     120        wrapper.load = cache.load 
     121        wrapper.dump = cache.dump 
     122        wrapper.archive = archive 
     123        wrapper.archived = cache.archived 
    59124       #wrapper._cache = None  #XXX 
    60125       #wrapper._queue = None  #XXX 
     
    65130 
    66131def inf_cache(*arg, **kwd): 
    67     '''Infinitely-growing cache decorator. 
     132    '''Infinitely-growing (INF) cache decorator. 
     133 
     134    This decorator memoizes a function's return value each time it is called. 
     135    If called later with the same arguments, the cached value is returned, and 
     136    not re-evaluated.  This cache will grow without bound.  To avoid memory 
     137    issues, it is suggested to frequently dump and clear the cache.  This 
     138    decorator takes an integer tolerance 'tol', equal to the number of decimal 
     139    places to which it will round off floats, and a bool 'deep' for whether the 
     140    rounding on inputs will be 'shallow' or 'deep'. 
     141 
     142    cache = storage hashmap (default is {}) 
     143    keymap = cache key encoder (default is keymaps.hashmap(flat=True)) 
     144    tol = integer tolerance for rounding (default is None) 
     145    deep = boolean for rounding depth (default is False, i.e. 'shallow') 
    68146 
    69147    If *keymap* is given, it will replace the hashing algorithm for generating 
    70     cache keys.  Several hashing algorithms are available in 'keymaps.py'.  With 
    71     the default keymap, arguments to the cached function must be hashable. 
     148    cache keys.  Several hashing algorithms are available in 'keymaps'. The 
     149    default keymap requires arguments to the cached function to be hashable. 
    72150 
    73151    If the keymap retains type information, then arguments of different types 
     
    76154    and may also be ignored by some 'keymaps'. 
    77155 
    78     View the cache statistics named tuple (hits, misses, maxsize, currsize) with 
    79     f.info().  Clear the cache and statistics with f.clear().  Access the 
    80     underlying function with f.__wrapped__.  This cache will grow without bound. 
    81  
     156    View cache statistics (hit, miss, load, maxsize, size) with f.info(). 
     157    Clear the cache and statistics with f.clear().  Replace the cache archive 
     158    with f.archive(obj).  Load from the archive with f.load(), and dump from 
     159    the cache to the archive with f.dump(). 
    82160    ''' 
    83161    maxsize = None 
    84     make_key = kwd.get('keymap', None) 
    85     if make_key is None: make_key = _keymap() 
     162 
     163    keymap = kwd.get('keymap', None) 
     164    if keymap is None: keymap = hashmap(flat=True) #XXX: stringmap ? 
     165    cache = kwd.get('cache', None) 
     166    if cache is None: cache = archive_dict() 
     167    elif type(cache) is dict: cache = archive_dict(cache) 
     168    # does archive make sense with database, file, ?... (requires more thought) 
     169 
     170    tol = kwd.get('tol', None) 
     171    deep = kwd.get('deep', False) 
     172    if deep: rounded = deep_round 
     173    else: rounded = simple_round 
     174   #else: rounded = shallow_round #FIXME: slow 
     175 
     176    @rounded(tol) 
     177    def rounded_args(*args, **kwds): 
     178        return (args, kwds) 
    86179 
    87180    def decorating_function(user_function): 
    88         cache = dict()                  # mapping of args to results 
    89         stats = [0, 0]                  # make statistics updateable non-locally 
    90         HITS, MISSES = 0, 1             # names for the stats fields 
     181       #cache = dict()                  # mapping of args to results 
     182        stats = [0, 0, 0]               # make statistics updateable non-locally 
     183        HIT, MISS, LOAD = 0, 1, 2       # names for the stats fields 
    91184       #_len = len                      # localize the global len() function 
    92185       #lock = RLock()                  # linkedlist updates aren't threadsafe 
    93186 
    94187        def wrapper(*args, **kwds): 
    95             key = make_key(*args, **kwds) 
    96  
    97             # get cache entry or compute if not found 
     188            #XXX: add try/except... any failure has result = f(*args, **kwds) 
     189            _args, _kwds = rounded_args(*args, **kwds) 
     190            key = keymap(*_args, **_kwds) 
     191 
    98192            try: 
     193                # get cache entry 
    99194                result = cache[key] 
    100                 stats[HITS] += 1 
     195                stats[HIT] += 1 
    101196            except KeyError: 
    102                 result = user_function(*args, **kwds) 
    103                 cache[key] = result 
    104                 stats[MISSES] += 1 
     197                # if not in cache, look in archive 
     198                if cache.archived(): 
     199                    cache.load(key) 
     200                try: 
     201                    result = cache[key] 
     202                    stats[LOAD] += 1 
     203                except KeyError: 
     204                    # if not found, then compute 
     205                    result = user_function(*args, **kwds) 
     206                    cache[key] = result 
     207                    stats[MISS] += 1 
    105208            return result 
    106209 
    107         def clear(): 
     210        def archive(obj): 
     211            """Replace the cache archive""" 
     212            cache.archive = obj 
     213 
     214        def clear(keepstats=False): 
    108215            """Clear the cache and statistics""" 
    109216            cache.clear() 
    110             stats[:] = [0, 0] 
     217            if not keepstats: stats[:] = [0, 0, 0] 
    111218 
    112219        def info(): 
    113220            """Report cache statistics""" 
    114             return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) 
     221            return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 
    115222 
    116223        # interface 
     
    118225        wrapper.info = info 
    119226        wrapper.clear = clear 
     227        wrapper.load = cache.load 
     228        wrapper.dump = cache.dump 
     229        wrapper.archive = archive 
     230        wrapper.archived = cache.archived 
    120231       #wrapper._cache = cache #XXX 
    121232       #wrapper._queue = None  #XXX 
     
    125236 
    126237 
    127 def lfu_cache(maxsize=100, keymap=None): 
    128     '''Least-frequenty-used cache decorator. 
    129  
    130     If *maxsize* is None, the LFU algorithm will be supressed and this cache 
    131     will grow without bound. 
     238def lfu_cache(maxsize=100, cache=None, keymap=None, tol=None, deep=False): 
     239    '''Least-frequenty-used (LFU) cache decorator. 
     240 
     241    This decorator memoizes a function's return value each time it is called. 
     242    If called later with the same arguments, the cached value is returned, and 
     243    not re-evaluated.  To avoid memory issues, a maximum cache size is imposed. 
     244    For caches with an archive, the full cache dumps to archive upon reaching 
     245    maxsize. For caches without an archive, the LFU algorithm manages the cache. 
     246    This decorator takes an integer tolerance 'tol', equal to the number of 
     247    decimal places to which it will round off floats, and a bool 'deep' for 
     248    whether the rounding on inputs will be 'shallow' or 'deep'. 
     249 
     250    maxsize = maximum cache size 
     251    cache = storage hashmap (default is {}) 
     252    keymap = cache key encoder (default is keymaps.hashmap(flat=True)) 
     253    tol = integer tolerance for rounding (default is None) 
     254    deep = boolean for rounding depth (default is False, i.e. 'shallow') 
     255 
     256    If *maxsize* is None, this cache will grow without bound. 
    132257 
    133258    If *keymap* is given, it will replace the hashing algorithm for generating 
    134     cache keys.  Several hashing algorithms are available in 'keymaps.py'.  With 
    135     the default keymap, arguments to the cached function must be hashable. 
     259    cache keys.  Several hashing algorithms are available in 'keymaps'. The 
     260    default keymap requires arguments to the cached function to be hashable. 
    136261 
    137262    If the keymap retains type information, then arguments of different types 
     
    140265    and may also be ignored by some 'keymaps'. 
    141266 
    142     View the cache statistics named tuple (hits, misses, maxsize, currsize) with 
    143     f.info().  Clear the cache and statistics with f.clear().  Access the 
    144     underlying function with f.__wrapped__. 
     267    View cache statistics (hit, miss, load, maxsize, size) with f.info(). 
     268    Clear the cache and statistics with f.clear().  Replace the cache archive 
     269    with f.archive(obj).  Load from the archive with f.load(), and dump from 
     270    the cache to the archive with f.dump(). 
    145271 
    146272    See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Frequently_Used 
    147  
    148273    ''' 
    149     if maxsize == 0: return no_cache() 
    150     if keymap is None: make_key = _keymap() 
    151     else: make_key = keymap 
    152     if maxsize is None: return inf_cache(keymap=make_key) 
    153      
     274    if maxsize == 0: 
     275        return no_cache(cache=cache, keymap=keymep, tol=tol, deep=deep) 
     276    if maxsize is None: 
     277        return inf_cache(cache=cache, keymap=keymap, tol=tol, deep=deep) 
     278 
     279    if keymap is None: keymap = hashmap(flat=True) #XXX: stringmap ? 
     280    if cache is None: cache = archive_dict() 
     281    elif type(cache) is dict: cache = archive_dict(cache) 
     282    # does archive make sense with database, file, ?... (requires more thought) 
     283 
     284    if deep: rounded = deep_round 
     285    else: rounded = simple_round 
     286   #else: rounded = shallow_round #FIXME: slow 
     287 
     288    @rounded(tol) 
     289    def rounded_args(*args, **kwds): 
     290        return (args, kwds) 
     291 
    154292    def decorating_function(user_function): 
    155         cache = dict()                  # mapping of args to results 
     293       #cache = dict()                  # mapping of args to results 
    156294        use_count = Counter()           # times each key has been accessed 
    157         stats = [0, 0]                  # make statistics updateable non-locally 
    158         HITS, MISSES = 0, 1             # names for the stats fields 
     295        stats = [0, 0, 0]               # make statistics updateable non-locally 
     296        HIT, MISS, LOAD = 0, 1, 2       # names for the stats fields 
    159297        _len = len                      # localize the global len() function 
    160298       #lock = RLock()                  # linkedlist updates aren't threadsafe 
    161299 
    162300        def wrapper(*args, **kwds): 
    163             key = make_key(*args, **kwds) 
    164  
    165             # get cache entry or compute if not found 
     301            #XXX: add try/except... any failure has result = f(*args, **kwds) 
     302            _args, _kwds = rounded_args(*args, **kwds) 
     303            key = keymap(*_args, **_kwds) 
     304 
    166305            try: 
     306                # get cache entry 
    167307                result = cache[key] 
    168308                use_count[key] += 1 
    169                 stats[HITS] += 1 
     309                stats[HIT] += 1 
    170310            except KeyError: 
    171                 cache[key] = user_function(*args, **kwds) 
    172                 # need to add something to the cache, make room if necessary 
    173                 if _len(cache) == maxsize: 
    174                     for k, _ in nsmallest(maxsize // 10 or 1, 
    175                                           use_count.iteritems(), 
    176                                           key=itemgetter(1)): 
    177                         del cache[k], use_count[k] 
    178                 result = cache[key] 
    179                 use_count[key] += 1 
    180                 stats[MISSES] += 1 
     311                # if not in cache, look in archive 
     312                if cache.archived(): 
     313                    cache.load(key) 
     314                try: 
     315                    result = cache[key] 
     316                    use_count[key] += 1 
     317                    stats[LOAD] += 1 
     318                except KeyError: 
     319                    # if not found, then compute 
     320                    result = user_function(*args, **kwds) 
     321                    cache[key] = result 
     322                    use_count[key] += 1 
     323                    stats[MISS] += 1 
     324 
     325                # purge cache 
     326                if _len(cache) > maxsize: 
     327                    if cache.archived(): 
     328                        cache.dump() 
     329                        cache.clear()  
     330                        use_count.clear() 
     331                    else: # purge least frequent cache entries 
     332                        for k, _ in nsmallest(max(2, maxsize // 10), 
     333                                              use_count.iteritems(), 
     334                                              key=itemgetter(1)): 
     335                            del cache[k], use_count[k] 
    181336            return result 
    182337 
    183         def clear(): 
     338        def archive(obj): 
     339            """Replace the cache archive""" 
     340            cache.archive = obj 
     341 
     342        def clear(keepstats=False): 
    184343            """Clear the cache and statistics""" 
    185344            cache.clear() 
    186345            use_count.clear() 
    187             stats[:] = [0, 0] 
     346            if not keepstats: stats[:] = [0, 0, 0] 
    188347 
    189348        def info(): 
    190349            """Report cache statistics""" 
    191             return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) 
     350            return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 
    192351 
    193352        # interface 
     
    195354        wrapper.info = info 
    196355        wrapper.clear = clear 
     356        wrapper.load = cache.load 
     357        wrapper.dump = cache.dump 
     358        wrapper.archive = archive 
     359        wrapper.archived = cache.archived 
    197360       #wrapper._cache = cache #XXX 
    198361       #wrapper._queue = use_count #XXX 
     
    202365 
    203366 
    204 def lru_cache(maxsize=100, keymap=None): 
    205     '''Least-recently-used cache decorator. 
    206  
    207     If *maxsize* is None, the LRU algorithm will be supressed and this cache 
    208     will grow without bound. 
     367def lru_cache(maxsize=100, cache=None, keymap=None, tol=None, deep=False): 
     368    '''Least-recently-used (LRU) cache decorator. 
     369 
     370    This decorator memoizes a function's return value each time it is called. 
     371    If called later with the same arguments, the cached value is returned, and 
     372    not re-evaluated.  To avoid memory issues, a maximum cache size is imposed. 
     373    For caches with an archive, the full cache dumps to archive upon reaching 
     374    maxsize. For caches without an archive, the LRU algorithm manages the cache. 
     375    This decorator takes an integer tolerance 'tol', equal to the number of 
     376    decimal places to which it will round off floats, and a bool 'deep' for 
     377    whether the rounding on inputs will be 'shallow' or 'deep'. 
     378 
     379    maxsize = maximum cache size 
     380    cache = storage hashmap (default is {}) 
     381    keymap = cache key encoder (default is keymaps.hashmap(flat=True)) 
     382    tol = integer tolerance for rounding (default is None) 
     383    deep = boolean for rounding depth (default is False, i.e. 'shallow') 
     384 
     385    If *maxsize* is None, this cache will grow without bound. 
    209386 
    210387    If *keymap* is given, it will replace the hashing algorithm for generating 
    211     cache keys.  Several hashing algorithms are available in 'keymaps.py'.  With 
    212     the default keymap, arguments to the cached function must be hashable. 
     388    cache keys.  Several hashing algorithms are available in 'keymaps'. The 
     389    default keymap requires arguments to the cached function to be hashable. 
    213390 
    214391    If the keymap retains type information, then arguments of different types 
     
    217394    and may also be ignored by some 'keymaps'. 
    218395 
    219     View the cache statistics named tuple (hits, misses, maxsize, currsize) with 
    220     f.info().  Clear the cache and statistics with f.clear().  Access the 
    221     underlying function with f.__wrapped__. 
     396    View cache statistics (hit, miss, load, maxsize, size) with f.info(). 
     397    Clear the cache and statistics with f.clear().  Replace the cache archive 
     398    with f.archive(obj).  Load from the archive with f.load(), and dump from 
     399    the cache to the archive with f.dump(). 
    222400 
    223401    See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used 
    224  
    225402    ''' 
    226     if maxsize == 0: return no_cache() 
    227     if keymap is None: make_key = _keymap() 
    228     else: make_key = keymap 
    229     if maxsize is None: return inf_cache(keymap=make_key) 
     403    if maxsize == 0: 
     404        return no_cache(cache=cache, keymap=keymep, tol=tol, deep=deep) 
     405    if maxsize is None: 
     406        return inf_cache(cache=cache, keymap=keymap, tol=tol, deep=deep) 
    230407    maxqueue = maxsize * 10 #XXX: user settable? confirm this works as expected 
    231408 
     409    if keymap is None: keymap = hashmap(flat=True) #XXX: stringmap ? 
     410    if cache is None: cache = archive_dict() 
     411    elif type(cache) is dict: cache = archive_dict(cache) 
     412    # does archive make sense with database, file, ?... (requires more thought) 
     413 
     414    if deep: rounded = deep_round 
     415    else: rounded = simple_round 
     416   #else: rounded = shallow_round #FIXME: slow 
     417 
     418    @rounded(tol) 
     419    def rounded_args(*args, **kwds): 
     420        return (args, kwds) 
     421 
    232422    def decorating_function(user_function): 
    233         cache = dict()                  # mapping of args to results 
     423       #cache = dict()                  # mapping of args to results 
    234424        queue = deque()                 # order that keys have been used 
    235425        refcount = Counter()            # times each key is in the queue 
    236426        sentinel = object()             # marker for looping around the queue 
    237         stats = [0, 0]                  # make statistics updateable non-locally 
    238         HITS, MISSES = 0, 1             # names for the stats fields 
     427        stats = [0, 0, 0]               # make statistics updateable non-locally 
     428        HIT, MISS, LOAD = 0, 1, 2       # names for the stats fields 
    239429        _len = len                      # localize the global len() function 
    240430       #lock = RLock()                  # linkedlist updates aren't threadsafe 
     
    245435 
    246436        def wrapper(*args, **kwds): 
    247             key = make_key(*args, **kwds) 
    248  
    249             # get cache entry or compute if not found 
     437            #XXX: add try/except... any failure has result = f(*args, **kwds) 
     438            _args, _kwds = rounded_args(*args, **kwds) 
     439            key = keymap(*_args, **_kwds) 
     440 
    250441            try: 
     442                # get cache entry 
    251443                result = cache[key] 
    252444                # record recent use of this key 
    253445                queue_append(key) 
    254446                refcount[key] += 1 
    255                 stats[HITS] += 1 
     447                stats[HIT] += 1 
    256448            except KeyError: 
    257                 result = user_function(*args, **kwds) 
    258                 cache[key] = result 
    259                 # record recent use of this key 
    260                 queue_append(key) 
    261                 refcount[key] += 1 
    262                 stats[MISSES] += 1 
    263  
    264                 # purge least recently used cache entry 
     449                # if not in cache, look in archive 
     450                if cache.archived(): 
     451                    cache.load(key) 
     452                try: 
     453                    result = cache[key] 
     454                    # record recent use of this key 
     455                    queue_append(key) 
     456                    refcount[key] += 1 
     457                    stats[LOAD] += 1 
     458                except KeyError: 
     459                    # if not found, then compute 
     460                    result = user_function(*args, **kwds) 
     461                    cache[key] = result 
     462                    # record recent use of this key 
     463                    queue_append(key) 
     464                    refcount[key] += 1 
     465                    stats[MISS] += 1 
     466 
     467                # purge cache 
    265468                if _len(cache) > maxsize: 
    266                     key = queue_popleft() 
    267                     refcount[key] -= 1 
    268                     while refcount[key]: 
     469                    if cache.archived(): 
     470                        cache.dump() 
     471                        cache.clear()  
     472                        queue.clear() 
     473                        refcount.clear() 
     474                    else: # purge least recently used cache entry 
    269475                        key = queue_popleft() 
    270476                        refcount[key] -= 1 
    271                     del cache[key], refcount[key] 
     477                        while refcount[key]: 
     478                            key = queue_popleft() 
     479                            refcount[key] -= 1 
     480                        del cache[key], refcount[key] 
    272481 
    273482            # periodically compact the queue by eliminating duplicate keys 
     
    282491            return result 
    283492 
    284         def clear(): 
     493        def archive(obj): 
     494            """Replace the cache archive""" 
     495            cache.archive = obj 
     496 
     497        def clear(keepstats=False): 
    285498            """Clear the cache and statistics""" 
    286499            cache.clear() 
    287500            queue.clear() 
    288501            refcount.clear() 
    289             stats[:] = [0, 0] 
     502            if not keepstats: stats[:] = [0, 0, 0] 
    290503 
    291504        def info(): 
    292505            """Report cache statistics""" 
    293             return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) 
     506            return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 
    294507 
    295508        # interface 
     
    297510        wrapper.info = info 
    298511        wrapper.clear = clear 
     512        wrapper.load = cache.load 
     513        wrapper.dump = cache.dump 
     514        wrapper.archive = archive 
     515        wrapper.archived = cache.archived 
    299516       #wrapper._cache = cache #XXX 
    300517       #wrapper._queue = queue #XXX 
     
    304521 
    305522 
    306 def mru_cache(maxsize=100, keymap=None): 
    307     '''Most-recently-used cache decorator. 
    308  
    309     If *maxsize* is None, the MRU algorithm will be supressed and this cache 
    310     will grow without bound. 
     523def mru_cache(maxsize=100, cache=None, keymap=None, tol=None, deep=False): 
     524    '''Most-recently-used (MRU) cache decorator. 
     525 
     526    This decorator memoizes a function's return value each time it is called. 
     527    If called later with the same arguments, the cached value is returned, and 
     528    not re-evaluated.  To avoid memory issues, a maximum cache size is imposed. 
     529    For caches with an archive, the full cache dumps to archive upon reaching 
     530    maxsize. For caches without an archive, the MRU algorithm manages the cache. 
     531    This decorator takes an integer tolerance 'tol', equal to the number of 
     532    decimal places to which it will round off floats, and a bool 'deep' for 
     533    whether the rounding on inputs will be 'shallow' or 'deep'. 
     534 
     535    maxsize = maximum cache size 
     536    cache = storage hashmap (default is {}) 
     537    keymap = cache key encoder (default is keymaps.hashmap(flat=True)) 
     538    tol = integer tolerance for rounding (default is None) 
     539    deep = boolean for rounding depth (default is False, i.e. 'shallow') 
     540 
     541    If *maxsize* is None, this cache will grow without bound. 
    311542 
    312543    If *keymap* is given, it will replace the hashing algorithm for generating 
    313     cache keys.  Several hashing algorithms are available in 'keymaps.py'.  With 
    314     the default keymap, arguments to the cached function must be hashable. 
     544    cache keys.  Several hashing algorithms are available in 'keymaps'. The 
     545    default keymap requires arguments to the cached function to be hashable. 
    315546 
    316547    If the keymap retains type information, then arguments of different types 
     
    319550    and may also be ignored by some 'keymaps'. 
    320551 
    321     View the cache statistics named tuple (hits, misses, maxsize, currsize) with 
    322     f.info().  Clear the cache and statistics with f.clear().  Access the 
    323     underlying function with f.__wrapped__. 
     552    View cache statistics (hit, miss, load, maxsize, size) with f.info(). 
     553    Clear the cache and statistics with f.clear().  Replace the cache archive 
     554    with f.archive(obj).  Load from the archive with f.load(), and dump from 
     555    the cache to the archive with f.dump(). 
    324556 
    325557    See: http://en.wikipedia.org/wiki/Cache_algorithms#Most_Recently_Used 
    326  
    327558    ''' 
    328     if maxsize == 0: return no_cache() 
    329     if keymap is None: make_key = _keymap() 
    330     else: make_key = keymap 
    331     if maxsize is None: return inf_cache(keymap=make_key) 
     559    if maxsize == 0: 
     560        return no_cache(cache=cache, keymap=keymep, tol=tol, deep=deep) 
     561    if maxsize is None: 
     562        return inf_cache(cache=cache, keymap=keymap, tol=tol, deep=deep) 
     563 
     564    if keymap is None: keymap = hashmap(flat=True) #XXX: stringmap ? 
     565    if cache is None: cache = archive_dict() 
     566    elif type(cache) is dict: cache = archive_dict(cache) 
     567    # does archive make sense with database, file, ?... (requires more thought) 
     568 
     569    if deep: rounded = deep_round 
     570    else: rounded = simple_round 
     571   #else: rounded = shallow_round #FIXME: slow 
     572 
     573    @rounded(tol) 
     574    def rounded_args(*args, **kwds): 
     575        return (args, kwds) 
    332576 
    333577    def decorating_function(user_function): 
    334         cache = dict()                  # mapping of args to results 
     578       #cache = dict()                  # mapping of args to results 
    335579        queue = deque()                 # order that keys have been used 
    336         stats = [0, 0]                  # make statistics updateable non-locally 
    337         HITS, MISSES = 0, 1             # names for the stats fields 
     580        stats = [0, 0, 0]               # make statistics updateable non-locally 
     581        HIT, MISS, LOAD = 0, 1, 2       # names for the stats fields 
    338582        _len = len                      # localize the global len() function 
    339583       #lock = RLock()                  # linkedlist updates aren't threadsafe 
     
    344588 
    345589        def wrapper(*args, **kwds): 
    346             key = make_key(*args, **kwds) 
    347  
    348             # get cache entry or compute if not found 
     590            #XXX: add try/except... any failure has result = f(*args, **kwds) 
     591            _args, _kwds = rounded_args(*args, **kwds) 
     592            key = keymap(*_args, **_kwds) 
     593 
    349594            try: 
     595                # get cache entry 
    350596                result = cache[key] 
    351597                queue.remove(key) 
    352                 stats[HITS] += 1 
     598                stats[HIT] += 1 
    353599            except KeyError: 
    354                 result = user_function(*args, **kwds) 
    355                 cache[key] = result 
    356                 stats[MISSES] += 1 
    357  
    358                 # purge most recently used cache entry 
     600                # if not in cache, look in archive 
     601                if cache.archived(): 
     602                    cache.load(key) 
     603                try: 
     604                    result = cache[key] 
     605                    stats[LOAD] += 1 
     606                except KeyError: 
     607                    # if not found, then compute 
     608                    result = user_function(*args, **kwds) 
     609                    cache[key] = result 
     610                    stats[MISS] += 1 
     611 
     612                # purge cache 
    359613                if _len(cache) > maxsize: 
    360                     del cache[queue_pop()] 
     614                    if cache.archived(): 
     615                        cache.dump() 
     616                        cache.clear()  
     617                        queue.clear() 
     618                    else: # purge most recently used cache entry 
     619                        del cache[queue_pop()] 
    361620 
    362621            # record recent use of this key 
     
    364623            return result 
    365624 
    366         def clear(): 
     625        def archive(obj): 
     626            """Replace the cache archive""" 
     627            cache.archive = obj 
     628 
     629        def clear(keepstats=False): 
    367630            """Clear the cache and statistics""" 
    368631            cache.clear() 
    369632            queue.clear() 
    370             stats[:] = [0, 0] 
     633            if not keepstats: stats[:] = [0, 0, 0] 
    371634 
    372635        def info(): 
    373636            """Report cache statistics""" 
    374             return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) 
     637            return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 
    375638 
    376639        # interface 
     
    378641        wrapper.info = info 
    379642        wrapper.clear = clear 
     643        wrapper.load = cache.load 
     644        wrapper.dump = cache.dump 
     645        wrapper.archive = archive 
     646        wrapper.archived = cache.archived 
    380647       #wrapper._cache = cache #XXX 
    381648       #wrapper._queue = queue #XXX 
     
    385652 
    386653 
    387 def rr_cache(maxsize=100, keymap=None): 
    388     '''random-replacement cache decorator. 
    389  
    390     If *maxsize* is None, the RR algorithm will be supressed and this cache 
    391     will grow without bound. 
     654def rr_cache(maxsize=100, cache=None, keymap=None, tol=None, deep=False): 
     655    '''random-replacement (RR) cache decorator. 
     656 
     657    This decorator memoizes a function's return value each time it is called. 
     658    If called later with the same arguments, the cached value is returned, and 
     659    not re-evaluated.  To avoid memory issues, a maximum cache size is imposed. 
     660    For caches with an archive, the full cache dumps to archive upon reaching 
     661    maxsize. For caches without an archive, the RR algorithm manages the cache. 
     662    This decorator takes an integer tolerance 'tol', equal to the number of 
     663    decimal places to which it will round off floats, and a bool 'deep' for 
     664    whether the rounding on inputs will be 'shallow' or 'deep'. 
     665 
     666    maxsize = maximum cache size 
     667    cache = storage hashmap (default is {}) 
     668    keymap = cache key encoder (default is keymaps.hashmap(flat=True)) 
     669    tol = integer tolerance for rounding (default is None) 
     670    deep = boolean for rounding depth (default is False, i.e. 'shallow') 
     671 
     672    If *maxsize* is None, this cache will grow without bound. 
    392673 
    393674    If *keymap* is given, it will replace the hashing algorithm for generating 
    394     cache keys.  Several hashing algorithms are available in 'keymaps.py'.  With 
    395     the default keymap, arguments to the cached function must be hashable. 
     675    cache keys.  Several hashing algorithms are available in 'keymaps'. The 
     676    default keymap requires arguments to the cached function to be hashable. 
    396677 
    397678    If the keymap retains type information, then arguments of different types 
     
    400681    and may also be ignored by some 'keymaps'. 
    401682 
    402     View the cache statistics named tuple (hits, misses, maxsize, currsize) with 
    403     f.info().  Clear the cache and statistics with f.clear().  Access the 
    404     underlying function with f.__wrapped__. 
     683    View cache statistics (hit, miss, load, maxsize, size) with f.info(). 
     684    Clear the cache and statistics with f.clear().  Replace the cache archive 
     685    with f.archive(obj).  Load from the archive with f.load(), and dump from 
     686    the cache to the archive with f.dump(). 
    405687 
    406688    http://en.wikipedia.org/wiki/Cache_algorithms#Random_Replacement 
    407  
    408689    ''' 
    409     if maxsize == 0: return no_cache() 
    410     if keymap is None: make_key = _keymap() 
    411     else: make_key = keymap 
    412     if maxsize is None: return inf_cache(keymap=make_key) 
     690    if maxsize == 0: 
     691        return no_cache(cache=cache, keymap=keymep, tol=tol, deep=deep) 
     692    if maxsize is None: 
     693        return inf_cache(cache=cache, keymap=keymap, tol=tol, deep=deep) 
     694 
     695    if keymap is None: keymap = hashmap(flat=True) #XXX: stringmap ? 
     696    if cache is None: cache = archive_dict() 
     697    elif type(cache) is dict: cache = archive_dict(cache) 
     698    # does archive make sense with database, file, ?... (requires more thought) 
     699 
     700    if deep: rounded = deep_round 
     701    else: rounded = simple_round 
     702   #else: rounded = shallow_round #FIXME: slow 
     703 
     704    @rounded(tol) 
     705    def rounded_args(*args, **kwds): 
     706        return (args, kwds) 
    413707 
    414708    def decorating_function(user_function): 
    415         cache = dict()                  # mapping of args to results 
    416         stats = [0, 0]                  # make statistics updateable non-locally 
    417         HITS, MISSES = 0, 1             # names for the stats fields 
     709       #cache = dict()                  # mapping of args to results 
     710        stats = [0, 0, 0]               # make statistics updateable non-locally 
     711        HIT, MISS, LOAD = 0, 1, 2       # names for the stats fields 
    418712        _len = len                      # localize the global len() function 
    419713       #lock = RLock()                  # linkedlist updates aren't threadsafe 
    420714 
    421715        def wrapper(*args, **kwds): 
    422             key = make_key(*args, **kwds) 
    423  
    424             # get cache entry or compute if not found 
     716            #XXX: add try/except... any failure has result = f(*args, **kwds) 
     717            _args, _kwds = rounded_args(*args, **kwds) 
     718            key = keymap(*_args, **_kwds) 
     719 
    425720            try: 
     721                # get cache entry 
    426722                result = cache[key] 
    427                 stats[HITS] += 1 
     723                stats[HIT] += 1 
    428724            except KeyError: 
    429                 result = user_function(*args, **kwds) 
    430                 cache[key] = result 
    431                 stats[MISSES] += 1 
    432  
    433                 # purge random cache entry 
     725                # if not in cache, look in archive 
     726                if cache.archived(): 
     727                    cache.load(key) 
     728                try: 
     729                    result = cache[key] 
     730                    stats[LOAD] += 1 
     731                except KeyError: 
     732                    # if not found, then compute 
     733                    result = user_function(*args, **kwds) 
     734                    cache[key] = result 
     735                    stats[MISS] += 1 
     736 
     737                # purge cache 
    434738                if _len(cache) > maxsize: 
    435                     del cache[choice(cache.keys())] 
     739                    if cache.archived(): 
     740                        cache.dump() 
     741                        cache.clear()  
     742                    else: # purge random cache entry 
     743                        del cache[choice(cache.keys())] 
    436744            return result 
    437745 
    438         def clear(): 
     746        def archive(obj): 
     747            """Replace the cache archive""" 
     748            cache.archive = obj 
     749 
     750        def clear(keepstats=False): 
    439751            """Clear the cache and statistics""" 
    440752            cache.clear() 
    441             stats[:] = [0, 0] 
     753            if not keepstats: stats[:] = [0, 0, 0] 
    442754 
    443755        def info(): 
    444756            """Report cache statistics""" 
    445             return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) 
     757            return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 
    446758 
    447759        # interface 
     
    449761        wrapper.info = info 
    450762        wrapper.clear = clear 
    451        #wrapper._cache = cache #XXX 
     763        wrapper.load = cache.load 
     764        wrapper.dump = cache.dump 
     765        wrapper.archive = archive 
     766        wrapper.archived = cache.archived 
     767       #wrapper._cache = None  #XXX 
    452768       #wrapper._queue = None  #XXX 
    453769        return update_wrapper(wrapper, user_function) 
     
    456772 
    457773 
    458 def _test_hits(algorithm, maxsize=20, keymap=None, rangelimit=5, tries=1000): 
     774def _test_hits(algorithm, maxsize=20, keymap=None, 
     775               rangelimit=5, tries=1000, archived=False): 
    459776 
    460777    @algorithm(maxsize=maxsize, keymap=keymap) 
    461778    def f(x, y): 
    462779        return 3*x+y 
     780 
     781    if archived: 
     782        from archives import file_archive 
     783        f.archive(file_archive('cache.pkl')) 
    463784 
    464785    domain = range(rangelimit) 
     
    468789        r = f(choice(domain), choice(domain)) 
    469790 
     791    f.dump() 
    470792    print (f.info()) 
    471793 
     
    473795if __name__ == '__main__': 
    474796 
    475     for cache in [rr_cache, mru_cache, lru_cache, lfu_cache, inf_cache]: 
     797    print "WITHOUT ARCHIVE" 
     798    for cache in [rr_cache,mru_cache,lru_cache,lfu_cache,inf_cache,no_cache]: 
    476799        print cache.__name__, ":",  
    477800        _test_hits(cache, maxsize=1000, rangelimit=50) 
    478801 
     802    print "\nWITH ARCHIVE" 
     803    for cache in [rr_cache,mru_cache,lru_cache,lfu_cache,inf_cache,no_cache]: 
     804        print cache.__name__, ":",  
     805        _test_hits(cache, maxsize=1000, rangelimit=50, archived=True) 
    479806 
    480807# EOF 
  • branches/decorate/memoize.py

    r664 r665  
    33""" 
    44from keymaps import stringmap 
     5from archives import archive_dict 
     6from rounding import deep_round, simple_round 
    57 
    6 __all__ = ['memoize','memoized','archive_dict',\ 
    7            'null_archive','file_archive','db_archive'] 
     8__all__ = ['memoize', 'memoized'] 
    89 
    9 def isiterable(x): 
    10  #try: 
    11  #    from collections import Iterable 
    12  #    return isinstance(x, Iterable) 
    13  #except ImportError: 
    14   try: 
    15     iter(x) 
    16     return True 
    17   except TypeError: return False 
    18  #return hasattr(x, '__len__') or hasattr(x, '__iter__') 
    19  
    20  
    21 def deep_round_factory(tol): 
    22   def deep_round(*args, **kwds): 
    23     argstype = type(args)  
    24     _args = list(args) 
    25     _kwds = kwds.copy() 
    26     for i,j in enumerate(args): 
    27       if isinstance(j, float): _args[i] = round(j, tol) # don't round int 
    28       elif isinstance(j, (str, unicode, type(BaseException()))): continue 
    29       elif isinstance(j, dict): _args[i] = deep_round(**j)[1] 
    30       elif isiterable(j): #XXX: fails on the above, so don't iterate them 
    31         jtype = type(j) 
    32         _args[i] = jtype(deep_round(*j)[0]) 
    33     for i,j in kwds.items(): 
    34       if isinstance(j, float): _kwds[i] = round(j, tol) 
    35       elif isinstance(j, (str, unicode, type(BaseException()))): continue 
    36       elif isinstance(j, dict): _kwds[i] = deep_round(**j)[1] 
    37       elif isiterable(j): #XXX: fails on the above, so don't iterate them 
    38         jtype = type(j) 
    39         _kwds[i] = jtype(deep_round(*j)[0]) 
    40     return argstype(_args), _kwds 
    41   return deep_round 
    42  
    43 """ 
    44 >>> deep_round = deep_round_factory(tol=0)  #FIXME: convert to decorator !!! 
    45 >>> deep_round([1.12,2,{'x':1.23, 'y':[4.56,5.67]}], x=set([11.22,44,'hi'])) 
    46 (([1.0, 2, {'y': [5.0, 6.0], 'x': 1.0}],), {'x': set([11.0, 'hi', 44])}) 
    47 """ 
    48  
    49 def deep_round(tol=0): 
    50   def dec(f): 
    51     def func(*args, **kwds): 
    52       if tol is None: 
    53         _args,_kwds = args,kwds 
    54       else: 
    55         _deep_round = deep_round_factory(tol) 
    56         _args,_kwds = _deep_round(*args, **kwds) 
    57       return f(*_args, **_kwds) 
    58     return func 
    59   return dec 
    60  
    61  
    62 def simple_round_factory(tol): 
    63   def simple_round(*args, **kwds): 
    64     argstype = type(args)  
    65     _args = list(args) 
    66     _kwds = kwds.copy() 
    67     for i,j in enumerate(args): 
    68       if isinstance(j, float): _args[i] = round(j, tol) # don't round int 
    69     for i,j in kwds.items(): 
    70       if isinstance(j, float): _kwds[i] = round(j, tol) 
    71     return argstype(_args), _kwds 
    72   return simple_round 
    73  
    74 def simple_round(tol=0): #NOTE: only rounds floats, nothing else 
    75   def dec(f): 
    76     def func(*args, **kwds): 
    77       if tol is None: 
    78         _args,_kwds = args,kwds 
    79       else: 
    80         _simple_round = simple_round_factory(tol) 
    81         _args,_kwds = _simple_round(*args, **kwds) 
    82       return f(*_args, **_kwds) 
    83     return func 
    84   return dec 
    85  
    86  
    87 def shallow_round_factory(tol): 
    88   from numpy import round as around #XXX: try or numpy makes this slow 
    89   def shallow_round(*args, **kwds): 
    90     argstype = type(args)  
    91     _args = list(args) 
    92     _kwds = kwds.copy() 
    93     for i,j in enumerate(args): 
    94       try: 
    95         jtype = type(j) 
    96         _args[i] = jtype(around(j, tol)) 
    97       except: pass 
    98     for i,j in kwds.items(): 
    99       try: 
    100         jtype = type(j) 
    101         _kwds[i] = jtype(around(j, tol)) 
    102       except: pass 
    103     return argstype(_args), _kwds 
    104   return shallow_round 
    105  
    106 def shallow_round(tol=0): #NOTE: rounds floats, lists, arrays one level deep 
    107   def dec(f): 
    108     def func(*args, **kwds): 
    109       if tol is None: 
    110         _args,_kwds = args,kwds 
    111       else: 
    112         _shallow_round = shallow_round_factory(tol) 
    113         _args,_kwds = _shallow_round(*args, **kwds) 
    114       return f(*_args, **_kwds) 
    115     return func 
    116   return dec 
    117  
    118  
    119 #FIXME: the below need expiration of cache due to time, calls, etc... 
    120 #FIXME: memoize*_round fails when decorating a class method 
    121  
    122 class archive_dict(dict): 
    123     """dictionary augmented with an archive backend""" 
    124     def __init__(self, *args, **kwds): 
    125         """initialize a dictionary with an archive backend 
    126  
    127     Additional Inputs: 
    128         archive: instance of archive object 
    129         """ 
    130         self.__swap__ = null_archive() 
    131         self.__archive__ = kwds.pop('archive', null_archive()) 
    132         dict.__init__(self, *args, **kwds) 
    133         return 
    134     def load(self, *args): 
    135         """load archive contents 
    136  
    137     If arguments are given, only load the specified keys 
    138         """ 
    139         if not args: 
    140             self.update(self.archive.__asdict__()) 
    141         for arg in args: 
    142             try: 
    143                 self.update({arg:self.archive[arg]}) 
    144             except KeyError: 
    145                 pass 
    146         return 
    147     def dump(self, *args): 
    148         """dump contents to archive 
    149  
    150     If arguments are given, only dump the specified keys 
    151         """ 
    152         if not args: 
    153             self.archive.update(self) 
    154         for arg in args: 
    155             if self.has_key(arg): 
    156                 self.archive.update({arg:self.__getitem__(arg)}) 
    157         return 
    158     def archived(self, *on): 
    159         """check if the dict is archived, or toggle archiving 
    160  
    161     If on is True, turn on the archive; if on is False, turn off the archive 
    162         """ 
    163         L = len(on) 
    164         if not L: 
    165             return not isinstance(self.archive, null_archive) 
    166         if L > 1: 
    167             raise TypeError, "archived expected at most 1 argument, got %s" % str(L+1) 
    168         if bool(on[0]): 
    169             if not isinstance(self.__swap__, null_archive): 
    170                 self.__swap__, self.__archive__ = self.__archive__, self.__swap__ 
    171             elif isinstance(self.__archive__, null_archive): 
    172                 raise ValueError, "no valid archive has been set" 
    173         else: 
    174             if not isinstance(self.__archive__, null_archive): 
    175                 self.__swap__, self.__archive__ = self.__archive__, self.__swap__ 
    176     def __get_archive(self): 
    177        #if not isinstance(self.__archive__, null_archive): 
    178        #    return 
    179         return self.__archive__ 
    180     def __archive(self, archive): 
    181         if not isinstance(self.__swap__, null_archive): 
    182             self.__swap__, self.__archive__ = self.__archive__, self.__swap__ 
    183         self.__archive__ = archive 
    184     # interface 
    185     archive = property(__get_archive, __archive) 
    186     pass 
    187  
    188  
    189 class null_archive(dict): 
    190     """dictionary interface to nothing -- it's always empty""" 
    191     def __init__(self): 
    192         """initialize a permanently-empty dictionary""" 
    193         dict.__init__(self) 
    194         return 
    195     def __asdict__(self): 
    196         return self 
    197     def __setitem__(self, key, value): 
    198         pass 
    199     def update(self, adict, **kwds): 
    200         pass 
    201     def setdefault(self, key, *value): 
    202         return self.get(key, *value) 
    203     def __repr__(self): 
    204         return "archive(NULL)" 
    205     pass 
    206  
    207  
    208 class file_archive(dict): 
    209     """dictionary-style interface to a file""" 
    210     def __init__(self, filename=None, serialized=True): # False 
    211         """initialize a file with a synchronized dictionary interface 
    212  
    213     Inputs: 
    214         serialized: if True, pickle file contents; otherwise save python objects 
    215         filename: name of the file backend [default: memo.pkl or memo.py] 
    216         """ 
    217         import os 
    218         """filename = full filepath""" 
    219         if filename is None: 
    220             if serialized: filename = 'memo.pkl' #FIXME: need better default 
    221             else: filename = 'memo.py' #FIXME: need better default 
    222         self._filename = filename 
    223         self._serialized = serialized 
    224         if not os.path.exists(filename): 
    225             self.__save__({}) 
    226         return 
    227     def __asdict__(self): 
    228         if self._serialized: 
    229             try: 
    230                 f = open(self._filename, 'rb') 
    231                 import dill as pickle 
    232                 memo = pickle.load(f) 
    233                 f.close() 
    234             except: 
    235                 memo = {} 
    236         else: 
    237             import os 
    238             file = os.path.basename(self._filename) 
    239             root = os.path.realpath(self._filename).rstrip(file)[:-1] 
    240             curdir = os.path.realpath(os.curdir) 
    241             file = file.rstrip('.py') or file.rstrip('.pyc') \ 
    242                 or file.rstrip('.pyo') or file.rstrip('.pyd') 
    243             os.chdir(root) 
    244             exec 'from %s import memo' % file #FIXME: unsafe 
    245             os.chdir(curdir) 
    246         return memo 
    247     def __save__(self, memo=None): 
    248         if memo == None: return 
    249         if self._serialized: 
    250             try: 
    251                 f = open(self._filename, 'wb') 
    252                 import dill as pickle 
    253                 pickle.dump(memo, f) 
    254                 f.close() 
    255             except: 
    256                 pass  #XXX: warning? fail? 
    257         else: 
    258             open(self._filename, 'wb').write('memo = %s' % memo) 
    259         return 
    260     #FIXME: missing a bunch of __...__ 
    261     def __getitem__(self, key): 
    262         memo = self.__asdict__() 
    263         return memo[key] 
    264     def __iter__(self): 
    265         return self.__asdict__().iterkeys() 
    266     def __repr__(self): 
    267         return "archive(%s: %s)" % (self._filename, self.__asdict__()) 
    268     def __setitem__(self, key, value): 
    269         memo = self.__asdict__() 
    270         memo[key] = value 
    271         self.__save__(memo) 
    272         return 
    273     def clear(self): 
    274         self.__save__({}) 
    275         return 
    276     #FIXME: copy, fromkeys 
    277     def get(self, key, value=None): 
    278         memo = self.__asdict__() 
    279         return memo.get(key, value) 
    280     def has_key(self, key): 
    281         return key in self.__asdict__() 
    282     def items(self): 
    283         return list(self.iteritems()) 
    284     def iteritems(self): 
    285         return self.__asdict__().iteritems() 
    286     iterkeys = __iter__ 
    287     def itervalues(self): 
    288         return self.__asdict__().itervalues() 
    289     def keys(self): 
    290         return list(self.__iter__()) 
    291     def pop(self, key, *value): 
    292         memo = self.__asdict__() 
    293         res = memo.pop(key, *value) 
    294         self.__save__(memo) 
    295         return res 
    296     #FIXME: popitem 
    297     def setdefault(self, key, *value): 
    298         res = self.__asdict__().get(key, *value) 
    299         self.__setitem__(key, res) 
    300         return res 
    301     def update(self, adict, **kwds): 
    302         memo = self.__asdict__() 
    303         memo.update(adict, **kwds) 
    304         self.__save__(memo) 
    305         return 
    306     def values(self): 
    307         return list(self.itervalues()) 
    308     pass 
    309  
    310  
    311 #XXX: should inherit from object not dict ? 
    312 class db_archive(dict): #XXX: requires UTF-8 key 
    313     """dictionary-style interface to a database""" 
    314     def __init__(self, database=None, table=None): 
    315         """initialize a database with a synchronized dictionary interface 
    316  
    317     Inputs: 
    318         database: url of the database backend [default: :memory:] 
    319         table: name of the associated database table 
    320         """ 
    321         if database is None: database = ':memory:' 
    322         self._database = database 
    323         if table is None: table = 'memo' 
    324         self._table = table 
    325         import sqlite3 as db 
    326         self._conn = db.connect(database) 
    327         self._curs = self._conn.cursor() 
    328         sql = "create table if not exists %s(argstr, fval)" % table 
    329         self._curs.execute(sql) 
    330         return 
    331     def __asdict__(self): 
    332         sql = "select * from %s" % self._table 
    333         res = self._curs.execute(sql) 
    334         d = {} 
    335         [d.update({k:v}) for (k,v) in res] # always get the last one 
    336         return d 
    337     #FIXME: missing a bunch of __...__ 
    338     def __getitem__(self, key): 
    339         res = self._select_key_items(key) 
    340         if res: return res[-1][-1] # always get the last one 
    341         raise KeyError, key 
    342     def __iter__(self): 
    343         sql = "select argstr from %s" % self._table 
    344         return (k[-1] for k in set(self._curs.execute(sql))) 
    345     def __repr__(self): 
    346         return "archive(%s: %s)" % (self._table, self.__asdict__()) 
    347     def __setitem__(self, key, value): #XXX: maintains 'history' of values 
    348         sql = "insert into %s values(?,?)" % self._table 
    349         self._curs.execute(sql, (key,value)) 
    350         self._conn.commit() 
    351         return 
    352     def clear(self): 
    353         [self.pop(k) for k in self.keys()] # better delete table, add empty ? 
    354         return 
    355     #FIXME: copy, fromkeys 
    356     def get(self, key, value=None): 
    357         res = self._select_key_items(key) 
    358         if res: value = res[-1][-1] 
    359         return value 
    360     def has_key(self, key): 
    361         return bool(self._select_key_items(key)) 
    362     def items(self): 
    363        #return self.__asdict__().items() 
    364         return list(self.iteritems()) 
    365     def iteritems(self): 
    366         return ((k,self.__getitem__(k)) for k in self.__iter__()) 
    367     iterkeys = __iter__ 
    368     def itervalues(self): 
    369         return (self.__getitem__(k) for k in self.__iter__()) 
    370     def keys(self): 
    371        #return self.__asdict__().keys() 
    372         return list(self.__iter__()) 
    373     def pop(self, key, *value): 
    374         L = len(value) 
    375         if L > 1: 
    376             raise TypeError, "pop expected at most 2 arguments, got %s" % str(L+1) 
    377         res = self._select_key_items(key) 
    378         if res: 
    379             _value = res[-1][-1] 
    380         else: 
    381             if not L: raise KeyError, key 
    382             _value = value[0] 
    383         sql = "delete from %s where argstr = ?" % self._table 
    384         self._curs.execute(sql, (key,)) 
    385         self._conn.commit() 
    386         return _value  
    387     #FIXME: popitem 
    388     def setdefault(self, key, *value): 
    389         L = len(value) 
    390         if L > 1: 
    391             raise TypeError, "setvalue expected at most 2 arguments, got %s" % str(L+1) 
    392         res = self._select_key_items(key) 
    393         if res: 
    394             _value = res[-1][-1] 
    395         else: 
    396             if not L: _value = None 
    397             else: _value = value[0] 
    398             self.__setitem__(key, _value) 
    399         return _value 
    400     def update(self, adict, **kwds): 
    401         _dict = adict.copy() 
    402         _dict.update(**kwds) 
    403         [self.__setitem__(k,v) for (k,v) in _dict.items()] 
    404         return 
    405     def values(self): 
    406        #return self.__asdict__().values() 
    407         return list(self.itervalues()) 
    408     def _select_key_items(self, key): 
    409         '''Return a tuple of (key, value) pairs that match the specified key. 
    410         ''' 
    411         sql = "select * from %s where argstr = ?" % self._table 
    412         return tuple(self._curs.execute(sql, (key,))) 
    413     pass 
    414  
     10#XXX: use cache maxsize algorithms... where dump to archive if maxsize 
     11#XXX: memoized fails when decorating a class method ??? 
    41512 
    41613def memoized(cache=None, keymap=None, tol=None, deep=False): 
     
    45956    return dec 
    46057 
    461 #FIXME: use cache maxsize algorithms... where dump to archive if maxsize 
    46258 
    46359class memoize(object): 
  • branches/decorate/rounding.py

    r664 r665  
    11""" 
    2 decorators that cache results to memory, to file, or to a database 
     2decorators that provide rounding 
    33""" 
    4 from keymaps import stringmap 
    54 
    6 __all__ = ['memoize','memoized','archive_dict',\ 
    7            'null_archive','file_archive','db_archive'] 
     5__all__ = ['deep_round', 'shallow_round', 'simple_round'] 
    86 
    97def isiterable(x): 
     
    117115 
    118116 
    119 #FIXME: the below need expiration of cache due to time, calls, etc... 
    120 #FIXME: memoize*_round fails when decorating a class method 
    121  
    122 class archive_dict(dict): 
    123     """dictionary augmented with an archive backend""" 
    124     def __init__(self, *args, **kwds): 
    125         """initialize a dictionary with an archive backend 
    126  
    127     Additional Inputs: 
    128         archive: instance of archive object 
    129         """ 
    130         self.__swap__ = null_archive() 
    131         self.__archive__ = kwds.pop('archive', null_archive()) 
    132         dict.__init__(self, *args, **kwds) 
    133         return 
    134     def load(self, *args): 
    135         """load archive contents 
    136  
    137     If arguments are given, only load the specified keys 
    138         """ 
    139         if not args: 
    140             self.update(self.archive.__asdict__()) 
    141         for arg in args: 
    142             try: 
    143                 self.update({arg:self.archive[arg]}) 
    144             except KeyError: 
    145                 pass 
    146         return 
    147     def dump(self, *args): 
    148         """dump contents to archive 
    149  
    150     If arguments are given, only dump the specified keys 
    151         """ 
    152         if not args: 
    153             self.archive.update(self) 
    154         for arg in args: 
    155             if self.has_key(arg): 
    156                 self.archive.update({arg:self.__getitem__(arg)}) 
    157         return 
    158     def archived(self, *on): 
    159         """check if the dict is archived, or toggle archiving 
    160  
    161     If on is True, turn on the archive; if on is False, turn off the archive 
    162         """ 
    163         L = len(on) 
    164         if not L: 
    165             return not isinstance(self.archive, null_archive) 
    166         if L > 1: 
    167             raise TypeError, "archived expected at most 1 argument, got %s" % str(L+1) 
    168         if bool(on[0]): 
    169             if not isinstance(self.__swap__, null_archive): 
    170                 self.__swap__, self.__archive__ = self.__archive__, self.__swap__ 
    171             elif isinstance(self.__archive__, null_archive): 
    172                 raise ValueError, "no valid archive has been set" 
    173         else: 
    174             if not isinstance(self.__archive__, null_archive): 
    175                 self.__swap__, self.__archive__ = self.__archive__, self.__swap__ 
    176     def __get_archive(self): 
    177        #if not isinstance(self.__archive__, null_archive): 
    178        #    return 
    179         return self.__archive__ 
    180     def __archive(self, archive): 
    181         if not isinstance(self.__swap__, null_archive): 
    182             self.__swap__, self.__archive__ = self.__archive__, self.__swap__ 
    183         self.__archive__ = archive 
    184     # interface 
    185     archive = property(__get_archive, __archive) 
    186     pass 
    187  
    188  
    189 class null_archive(dict): 
    190     """dictionary interface to nothing -- it's always empty""" 
    191     def __init__(self): 
    192         """initialize a permanently-empty dictionary""" 
    193         dict.__init__(self) 
    194         return 
    195     def __asdict__(self): 
    196         return self 
    197     def __setitem__(self, key, value): 
    198         pass 
    199     def update(self, adict, **kwds): 
    200         pass 
    201     def setdefault(self, key, *value): 
    202         return self.get(key, *value) 
    203     def __repr__(self): 
    204         return "archive(NULL)" 
    205     pass 
    206  
    207  
    208 class file_archive(dict): 
    209     """dictionary-style interface to a file""" 
    210     def __init__(self, filename=None, serialized=True): # False 
    211         """initialize a file with a synchronized dictionary interface 
    212  
    213     Inputs: 
    214         serialized: if True, pickle file contents; otherwise save python objects 
    215         filename: name of the file backend [default: memo.pkl or memo.py] 
    216         """ 
    217         import os 
    218         """filename = full filepath""" 
    219         if filename is None: 
    220             if serialized: filename = 'memo.pkl' #FIXME: need better default 
    221             else: filename = 'memo.py' #FIXME: need better default 
    222         self._filename = filename 
    223         self._serialized = serialized 
    224         if not os.path.exists(filename): 
    225             self.__save__({}) 
    226         return 
    227     def __asdict__(self): 
    228         if self._serialized: 
    229             try: 
    230                 f = open(self._filename, 'rb') 
    231                 import dill as pickle 
    232                 memo = pickle.load(f) 
    233                 f.close() 
    234             except: 
    235                 memo = {} 
    236         else: 
    237             import os 
    238             file = os.path.basename(self._filename) 
    239             root = os.path.realpath(self._filename).rstrip(file)[:-1] 
    240             curdir = os.path.realpath(os.curdir) 
    241             file = file.rstrip('.py') or file.rstrip('.pyc') \ 
    242                 or file.rstrip('.pyo') or file.rstrip('.pyd') 
    243             os.chdir(root) 
    244             exec 'from %s import memo' % file #FIXME: unsafe 
    245             os.chdir(curdir) 
    246         return memo 
    247     def __save__(self, memo=None): 
    248         if memo == None: return 
    249         if self._serialized: 
    250             try: 
    251                 f = open(self._filename, 'wb') 
    252                 import dill as pickle 
    253                 pickle.dump(memo, f) 
    254                 f.close() 
    255             except: 
    256                 pass  #XXX: warning? fail? 
    257         else: 
    258             open(self._filename, 'wb').write('memo = %s' % memo) 
    259         return 
    260     #FIXME: missing a bunch of __...__ 
    261     def __getitem__(self, key): 
    262         memo = self.__asdict__() 
    263         return memo[key] 
    264     def __iter__(self): 
    265         return self.__asdict__().iterkeys() 
    266     def __repr__(self): 
    267         return "archive(%s: %s)" % (self._filename, self.__asdict__()) 
    268     def __setitem__(self, key, value): 
    269         memo = self.__asdict__() 
    270         memo[key] = value 
    271         self.__save__(memo) 
    272         return 
    273     def clear(self): 
    274         self.__save__({}) 
    275         return 
    276     #FIXME: copy, fromkeys 
    277     def get(self, key, value=None): 
    278         memo = self.__asdict__() 
    279         return memo.get(key, value) 
    280     def has_key(self, key): 
    281         return key in self.__asdict__() 
    282     def items(self): 
    283         return list(self.iteritems()) 
    284     def iteritems(self): 
    285         return self.__asdict__().iteritems() 
    286     iterkeys = __iter__ 
    287     def itervalues(self): 
    288         return self.__asdict__().itervalues() 
    289     def keys(self): 
    290         return list(self.__iter__()) 
    291     def pop(self, key, *value): 
    292         memo = self.__asdict__() 
    293         res = memo.pop(key, *value) 
    294         self.__save__(memo) 
    295         return res 
    296     #FIXME: popitem 
    297     def setdefault(self, key, *value): 
    298         res = self.__asdict__().get(key, *value) 
    299         self.__setitem__(key, res) 
    300         return res 
    301     def update(self, adict, **kwds): 
    302         memo = self.__asdict__() 
    303         memo.update(adict, **kwds) 
    304         self.__save__(memo) 
    305         return 
    306     def values(self): 
    307         return list(self.itervalues()) 
    308     pass 
    309  
    310  
    311 #XXX: should inherit from object not dict ? 
    312 class db_archive(dict): #XXX: requires UTF-8 key 
    313     """dictionary-style interface to a database""" 
    314     def __init__(self, database=None, table=None): 
    315         """initialize a database with a synchronized dictionary interface 
    316  
    317     Inputs: 
    318         database: url of the database backend [default: :memory:] 
    319         table: name of the associated database table 
    320         """ 
    321         if database is None: database = ':memory:' 
    322         self._database = database 
    323         if table is None: table = 'memo' 
    324         self._table = table 
    325         import sqlite3 as db 
    326         self._conn = db.connect(database) 
    327         self._curs = self._conn.cursor() 
    328         sql = "create table if not exists %s(argstr, fval)" % table 
    329         self._curs.execute(sql) 
    330         return 
    331     def __asdict__(self): 
    332         sql = "select * from %s" % self._table 
    333         res = self._curs.execute(sql) 
    334         d = {} 
    335         [d.update({k:v}) for (k,v) in res] # always get the last one 
    336         return d 
    337     #FIXME: missing a bunch of __...__ 
    338     def __getitem__(self, key): 
    339         res = self._select_key_items(key) 
    340         if res: return res[-1][-1] # always get the last one 
    341         raise KeyError, key 
    342     def __iter__(self): 
    343         sql = "select argstr from %s" % self._table 
    344         return (k[-1] for k in set(self._curs.execute(sql))) 
    345     def __repr__(self): 
    346         return "archive(%s: %s)" % (self._table, self.__asdict__()) 
    347     def __setitem__(self, key, value): #XXX: maintains 'history' of values 
    348         sql = "insert into %s values(?,?)" % self._table 
    349         self._curs.execute(sql, (key,value)) 
    350         self._conn.commit() 
    351         return 
    352     def clear(self): 
    353         [self.pop(k) for k in self.keys()] # better delete table, add empty ? 
    354         return 
    355     #FIXME: copy, fromkeys 
    356     def get(self, key, value=None): 
    357         res = self._select_key_items(key) 
    358         if res: value = res[-1][-1] 
    359         return value 
    360     def has_key(self, key): 
    361         return bool(self._select_key_items(key)) 
    362     def items(self): 
    363        #return self.__asdict__().items() 
    364         return list(self.iteritems()) 
    365     def iteritems(self): 
    366         return ((k,self.__getitem__(k)) for k in self.__iter__()) 
    367     iterkeys = __iter__ 
    368     def itervalues(self): 
    369         return (self.__getitem__(k) for k in self.__iter__()) 
    370     def keys(self): 
    371        #return self.__asdict__().keys() 
    372         return list(self.__iter__()) 
    373     def pop(self, key, *value): 
    374         L = len(value) 
    375         if L > 1: 
    376             raise TypeError, "pop expected at most 2 arguments, got %s" % str(L+1) 
    377         res = self._select_key_items(key) 
    378         if res: 
    379             _value = res[-1][-1] 
    380         else: 
    381             if not L: raise KeyError, key 
    382             _value = value[0] 
    383         sql = "delete from %s where argstr = ?" % self._table 
    384         self._curs.execute(sql, (key,)) 
    385         self._conn.commit() 
    386         return _value  
    387     #FIXME: popitem 
    388     def setdefault(self, key, *value): 
    389         L = len(value) 
    390         if L > 1: 
    391             raise TypeError, "setvalue expected at most 2 arguments, got %s" % str(L+1) 
    392         res = self._select_key_items(key) 
    393         if res: 
    394             _value = res[-1][-1] 
    395         else: 
    396             if not L: _value = None 
    397             else: _value = value[0] 
    398             self.__setitem__(key, _value) 
    399         return _value 
    400     def update(self, adict, **kwds): 
    401         _dict = adict.copy() 
    402         _dict.update(**kwds) 
    403         [self.__setitem__(k,v) for (k,v) in _dict.items()] 
    404         return 
    405     def values(self): 
    406        #return self.__asdict__().values() 
    407         return list(self.itervalues()) 
    408     def _select_key_items(self, key): 
    409         '''Return a tuple of (key, value) pairs that match the specified key. 
    410         ''' 
    411         sql = "select * from %s where argstr = ?" % self._table 
    412         return tuple(self._curs.execute(sql, (key,))) 
    413     pass 
    414  
    415  
    416 def memoized(cache=None, keymap=None, tol=None, deep=False): 
    417     """Decorator that memoizes a function's return value each time it is called. 
    418     If called later with the same arguments, the memoized value is returned, and 
    419     not re-evaluated.  This may lead to memory issues, as cache is not cleared. 
    420     This decorator takes an integer tolerance 'tol', equal to the number of 
    421     decimal places to which it will round off floats. 
    422  
    423     cache = storage hashmap (default is {}) 
    424     keymap = cache key encoder (default is keymaps.stringmap(flat=False)) 
    425     tol = integer tolerance for rounding (default is None) 
    426     deep = boolean for rounding depth (default is False, i.e. 'shallow') 
    427     """ 
    428     if keymap is None: keymap = stringmap(flat=False) 
    429     if cache is None: cache = archive_dict() 
    430     elif type(cache) is dict: cache = archive_dict(cache) 
    431     # does archive make sense with database, file, ?... (requires more thought) 
    432  
    433     if deep: rounded = deep_round 
    434     else: rounded = simple_round 
    435    #else: rounded = shallow_round #FIXME: slow 
    436  
    437     @rounded(tol) 
    438     def rounded_args(*args, **kwds): 
    439         return (args, kwds) 
    440  
    441     def dec(f): 
    442         def func(*args, **kwds): 
    443             try: 
    444                 _args, _kwds = rounded_args(*args, **kwds) 
    445                 argstr = keymap(*_args, **_kwds) 
    446                 if cache.has_key(argstr): 
    447                     return cache[argstr] 
    448                 if cache.archived(): 
    449                     cache.load(argstr) 
    450                     if cache.has_key(argstr): 
    451                         return cache[argstr] 
    452                 res = f(*args, **kwds) 
    453                 cache[argstr] = res  #XXX: any automated dump to archive? 
    454                 return res 
    455             except: #TypeError 
    456                 return f(*args, **kwds) 
    457         func.cache = cache 
    458         return func 
    459     return dec 
    460  
    461 #FIXME: use cache maxsize algorithms... where dump to archive if maxsize 
    462  
    463 class memoize(object): 
    464     """Decorator that memoizes a function's return value each time it is called. 
    465     If called later with the same arguments, the memoized value is returned, and 
    466     not re-evaluated.  This may lead to memory issues, as cache is not cleared. 
    467     Can memoize a *method* on an object. 
    468     """ 
    469     def __init__(self, cache=None, keymap=None, tol=None, deep=False): 
    470 #       self.func = func 
    471         if keymap is None: keymap = stringmap(flat=False) 
    472         if cache is None: cache = archive_dict() 
    473         elif type(cache) is dict: cache = archive_dict(cache) 
    474         self.cache = cache 
    475         self.__keymap = keymap 
    476  
    477         if deep: rounded = deep_round 
    478         else: rounded = simple_round 
    479  
    480         @rounded(tol) 
    481         def rounded_args(*args, **kwds): 
    482             return (args, kwds) 
    483         self.__rounded_args = rounded_args 
    484         return 
    485  
    486     def __call__(self, func): 
    487         self.func = func 
    488         import functools 
    489         @functools.wraps(func) 
    490         def dec(*args, **kwds): 
    491             try: 
    492                 _args, _kwds = self.__rounded_args(*args, **kwds) 
    493                 argstr = self.__keymap(*_args, **_kwds) 
    494                 if self.cache.has_key(argstr): 
    495                     return self.cache[argstr] 
    496                 if self.cache.archived(): 
    497                     self.cache.load(argstr) 
    498                     if self.cache.has_key(argstr): 
    499                         return self.cache[argstr] 
    500                 res = self.func(*args, **kwds) 
    501                 self.cache[argstr] = res 
    502                 return res 
    503             except: #TypeError 
    504                 return self.func(*args, **kwds) 
    505         return dec 
    506  
    507     def __repr__(self): 
    508         """Return the function's docstring.""" 
    509         return self.func.__doc__ 
    510  
    511     def __get__(self, obj, objtype): 
    512         """Support instance methods.""" 
    513         import functools 
    514         return functools.partial(self.__call__, obj) 
    515  
    516117# EOF 
  • branches/decorate/surrogate.py

    r664 r665  
    2020 
    2121 
    22 from memoize import memoized, archive_dict, file_archive 
    23 from keymaps import picklemap 
     22from memoize import memoized 
     23from archives import archive_dict, file_archive 
     24from keymaps import picklemap, hashmap 
    2425dumps = picklemap(flat=False) 
    2526cache = archive_dict(archive=file_archive('surrogate.pkl')) 
  • branches/decorate/test_cached_memoize.py

    r664 r665  
    1 from memoize import memoized, file_archive 
     1from memoize import memoized 
     2from archives import file_archive 
    23from timer import timed 
    34 
  • branches/decorate/test_memoize.py

    r664 r665  
    110110 
    111111 
    112 from memoize import archive_dict, db_archive  
     112from archives import archive_dict, db_archive  
    113113import dill 
    114114@memoized(cache=archive_dict(archive=db_archive())) 
Note: See TracChangeset for help on using the changeset viewer.