- Timestamp:
- 05/28/13 16:05:22 (3 years ago)
- Location:
- branches/decorate
- Files:
-
- 5 edited
- 2 copied
Legend:
- Unmodified
- Added
- Removed
-
branches/decorate/archives.py
r664 r665 1 1 """ 2 decorators that cache results to memory, to file, or to adatabase2 custom caching dict, which archives results to memory, file, or database 3 3 """ 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'] 121 6 122 7 class archive_dict(dict): … … 414 299 415 300 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, and419 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 of421 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_round434 else: rounded = simple_round435 #else: rounded = shallow_round #FIXME: slow436 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 res455 except: #TypeError456 return f(*args, **kwds)457 func.cache = cache458 return func459 return dec460 461 #FIXME: use cache maxsize algorithms... where dump to archive if maxsize462 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, and466 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 = func471 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 = cache475 self.__keymap = keymap476 477 if deep: rounded = deep_round478 else: rounded = simple_round479 480 @rounded(tol)481 def rounded_args(*args, **kwds):482 return (args, kwds)483 self.__rounded_args = rounded_args484 return485 486 def __call__(self, func):487 self.func = func488 import functools489 @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] = res502 return res503 except: #TypeError504 return self.func(*args, **kwds)505 return dec506 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 functools514 return functools.partial(self.__call__, obj)515 516 301 # EOF -
branches/decorate/cache.py
r663 r665 12 12 from functools import update_wrapper 13 13 from threading import RLock 14 from keymaps import hashmap as _keymap 15 16 _CacheInfo = namedtuple("CacheInfo", ['hits','misses','maxsize','currsize']) 14 from rounding import deep_round, simple_round 15 from archives import archive_dict 16 from keymaps import hashmap 17 18 _CacheInfo = namedtuple("CacheInfo", ['hit','miss','load','maxsize','size']) 17 19 18 20 class Counter(dict): … … 21 23 return 0 22 24 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 23 28 def 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(). 31 55 ''' 32 56 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) 34 71 35 72 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 38 76 _len = len # localize the global len() function 39 77 #lock = RLock() # linkedlist updates aren't threadsafe 40 78 41 79 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() 45 102 return result 46 103 47 def clear(): 104 def archive(obj): 105 """Replace the cache archive""" 106 cache.archive = obj 107 108 def clear(keepstats=False): 48 109 """Clear the cache and statistics""" 49 stats[:] = [0, 0]110 if not keepstats: stats[:] = [0, 0, 0] 50 111 51 112 def info(): 52 113 """Report cache statistics""" 53 return _CacheInfo(stats[HIT S], stats[MISSES], maxsize, len(cache))114 return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 54 115 55 116 # interface … … 57 118 wrapper.info = info 58 119 wrapper.clear = clear 120 wrapper.load = cache.load 121 wrapper.dump = cache.dump 122 wrapper.archive = archive 123 wrapper.archived = cache.archived 59 124 #wrapper._cache = None #XXX 60 125 #wrapper._queue = None #XXX … … 65 130 66 131 def 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') 68 146 69 147 If *keymap* is given, it will replace the hashing algorithm for generating 70 cache keys. Several hashing algorithms are available in 'keymaps .py'. With71 the default keymap, arguments to the cached function mustbe hashable.148 cache keys. Several hashing algorithms are available in 'keymaps'. The 149 default keymap requires arguments to the cached function to be hashable. 72 150 73 151 If the keymap retains type information, then arguments of different types … … 76 154 and may also be ignored by some 'keymaps'. 77 155 78 View the cache statistics named tuple (hits, misses, maxsize, currsize) with79 f.info(). Clear the cache and statistics with f.clear(). Access the80 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(). 82 160 ''' 83 161 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) 86 179 87 180 def decorating_function(user_function): 88 89 stats = [0, 0 ]# make statistics updateable non-locally90 HIT S, MISSES = 0, 1# names for the stats fields181 #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 91 184 #_len = len # localize the global len() function 92 185 #lock = RLock() # linkedlist updates aren't threadsafe 93 186 94 187 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 98 192 try: 193 # get cache entry 99 194 result = cache[key] 100 stats[HIT S] += 1195 stats[HIT] += 1 101 196 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 105 208 return result 106 209 107 def clear(): 210 def archive(obj): 211 """Replace the cache archive""" 212 cache.archive = obj 213 214 def clear(keepstats=False): 108 215 """Clear the cache and statistics""" 109 216 cache.clear() 110 stats[:] = [0, 0]217 if not keepstats: stats[:] = [0, 0, 0] 111 218 112 219 def info(): 113 220 """Report cache statistics""" 114 return _CacheInfo(stats[HIT S], stats[MISSES], maxsize, len(cache))221 return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 115 222 116 223 # interface … … 118 225 wrapper.info = info 119 226 wrapper.clear = clear 227 wrapper.load = cache.load 228 wrapper.dump = cache.dump 229 wrapper.archive = archive 230 wrapper.archived = cache.archived 120 231 #wrapper._cache = cache #XXX 121 232 #wrapper._queue = None #XXX … … 125 236 126 237 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. 238 def 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. 132 257 133 258 If *keymap* is given, it will replace the hashing algorithm for generating 134 cache keys. Several hashing algorithms are available in 'keymaps .py'. With135 the default keymap, arguments to the cached function mustbe hashable.259 cache keys. Several hashing algorithms are available in 'keymaps'. The 260 default keymap requires arguments to the cached function to be hashable. 136 261 137 262 If the keymap retains type information, then arguments of different types … … 140 265 and may also be ignored by some 'keymaps'. 141 266 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(). 145 271 146 272 See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Frequently_Used 147 148 273 ''' 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 154 292 def decorating_function(user_function): 155 293 #cache = dict() # mapping of args to results 156 294 use_count = Counter() # times each key has been accessed 157 stats = [0, 0 ]# make statistics updateable non-locally158 HIT S, MISSES = 0, 1# names for the stats fields295 stats = [0, 0, 0] # make statistics updateable non-locally 296 HIT, MISS, LOAD = 0, 1, 2 # names for the stats fields 159 297 _len = len # localize the global len() function 160 298 #lock = RLock() # linkedlist updates aren't threadsafe 161 299 162 300 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 166 305 try: 306 # get cache entry 167 307 result = cache[key] 168 308 use_count[key] += 1 169 stats[HIT S] += 1309 stats[HIT] += 1 170 310 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] 181 336 return result 182 337 183 def clear(): 338 def archive(obj): 339 """Replace the cache archive""" 340 cache.archive = obj 341 342 def clear(keepstats=False): 184 343 """Clear the cache and statistics""" 185 344 cache.clear() 186 345 use_count.clear() 187 stats[:] = [0, 0]346 if not keepstats: stats[:] = [0, 0, 0] 188 347 189 348 def info(): 190 349 """Report cache statistics""" 191 return _CacheInfo(stats[HIT S], stats[MISSES], maxsize, len(cache))350 return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 192 351 193 352 # interface … … 195 354 wrapper.info = info 196 355 wrapper.clear = clear 356 wrapper.load = cache.load 357 wrapper.dump = cache.dump 358 wrapper.archive = archive 359 wrapper.archived = cache.archived 197 360 #wrapper._cache = cache #XXX 198 361 #wrapper._queue = use_count #XXX … … 202 365 203 366 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. 367 def 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. 209 386 210 387 If *keymap* is given, it will replace the hashing algorithm for generating 211 cache keys. Several hashing algorithms are available in 'keymaps .py'. With212 the default keymap, arguments to the cached function mustbe hashable.388 cache keys. Several hashing algorithms are available in 'keymaps'. The 389 default keymap requires arguments to the cached function to be hashable. 213 390 214 391 If the keymap retains type information, then arguments of different types … … 217 394 and may also be ignored by some 'keymaps'. 218 395 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(). 222 400 223 401 See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used 224 225 402 ''' 226 if maxsize == 0: return no_cache()227 if keymap is None: make_key = _keymap()228 else: make_key = keymap229 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) 230 407 maxqueue = maxsize * 10 #XXX: user settable? confirm this works as expected 231 408 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 232 422 def decorating_function(user_function): 233 423 #cache = dict() # mapping of args to results 234 424 queue = deque() # order that keys have been used 235 425 refcount = Counter() # times each key is in the queue 236 426 sentinel = object() # marker for looping around the queue 237 stats = [0, 0 ]# make statistics updateable non-locally238 HIT S, MISSES = 0, 1# names for the stats fields427 stats = [0, 0, 0] # make statistics updateable non-locally 428 HIT, MISS, LOAD = 0, 1, 2 # names for the stats fields 239 429 _len = len # localize the global len() function 240 430 #lock = RLock() # linkedlist updates aren't threadsafe … … 245 435 246 436 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 250 441 try: 442 # get cache entry 251 443 result = cache[key] 252 444 # record recent use of this key 253 445 queue_append(key) 254 446 refcount[key] += 1 255 stats[HIT S] += 1447 stats[HIT] += 1 256 448 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 265 468 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 269 475 key = queue_popleft() 270 476 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] 272 481 273 482 # periodically compact the queue by eliminating duplicate keys … … 282 491 return result 283 492 284 def clear(): 493 def archive(obj): 494 """Replace the cache archive""" 495 cache.archive = obj 496 497 def clear(keepstats=False): 285 498 """Clear the cache and statistics""" 286 499 cache.clear() 287 500 queue.clear() 288 501 refcount.clear() 289 stats[:] = [0, 0]502 if not keepstats: stats[:] = [0, 0, 0] 290 503 291 504 def info(): 292 505 """Report cache statistics""" 293 return _CacheInfo(stats[HIT S], stats[MISSES], maxsize, len(cache))506 return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 294 507 295 508 # interface … … 297 510 wrapper.info = info 298 511 wrapper.clear = clear 512 wrapper.load = cache.load 513 wrapper.dump = cache.dump 514 wrapper.archive = archive 515 wrapper.archived = cache.archived 299 516 #wrapper._cache = cache #XXX 300 517 #wrapper._queue = queue #XXX … … 304 521 305 522 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. 523 def 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. 311 542 312 543 If *keymap* is given, it will replace the hashing algorithm for generating 313 cache keys. Several hashing algorithms are available in 'keymaps .py'. With314 the default keymap, arguments to the cached function mustbe hashable.544 cache keys. Several hashing algorithms are available in 'keymaps'. The 545 default keymap requires arguments to the cached function to be hashable. 315 546 316 547 If the keymap retains type information, then arguments of different types … … 319 550 and may also be ignored by some 'keymaps'. 320 551 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(). 324 556 325 557 See: http://en.wikipedia.org/wiki/Cache_algorithms#Most_Recently_Used 326 327 558 ''' 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) 332 576 333 577 def decorating_function(user_function): 334 578 #cache = dict() # mapping of args to results 335 579 queue = deque() # order that keys have been used 336 stats = [0, 0 ]# make statistics updateable non-locally337 HIT S, MISSES = 0, 1# names for the stats fields580 stats = [0, 0, 0] # make statistics updateable non-locally 581 HIT, MISS, LOAD = 0, 1, 2 # names for the stats fields 338 582 _len = len # localize the global len() function 339 583 #lock = RLock() # linkedlist updates aren't threadsafe … … 344 588 345 589 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 349 594 try: 595 # get cache entry 350 596 result = cache[key] 351 597 queue.remove(key) 352 stats[HIT S] += 1598 stats[HIT] += 1 353 599 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 359 613 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()] 361 620 362 621 # record recent use of this key … … 364 623 return result 365 624 366 def clear(): 625 def archive(obj): 626 """Replace the cache archive""" 627 cache.archive = obj 628 629 def clear(keepstats=False): 367 630 """Clear the cache and statistics""" 368 631 cache.clear() 369 632 queue.clear() 370 stats[:] = [0, 0]633 if not keepstats: stats[:] = [0, 0, 0] 371 634 372 635 def info(): 373 636 """Report cache statistics""" 374 return _CacheInfo(stats[HIT S], stats[MISSES], maxsize, len(cache))637 return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 375 638 376 639 # interface … … 378 641 wrapper.info = info 379 642 wrapper.clear = clear 643 wrapper.load = cache.load 644 wrapper.dump = cache.dump 645 wrapper.archive = archive 646 wrapper.archived = cache.archived 380 647 #wrapper._cache = cache #XXX 381 648 #wrapper._queue = queue #XXX … … 385 652 386 653 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. 654 def 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. 392 673 393 674 If *keymap* is given, it will replace the hashing algorithm for generating 394 cache keys. Several hashing algorithms are available in 'keymaps .py'. With395 the default keymap, arguments to the cached function mustbe hashable.675 cache keys. Several hashing algorithms are available in 'keymaps'. The 676 default keymap requires arguments to the cached function to be hashable. 396 677 397 678 If the keymap retains type information, then arguments of different types … … 400 681 and may also be ignored by some 'keymaps'. 401 682 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(). 405 687 406 688 http://en.wikipedia.org/wiki/Cache_algorithms#Random_Replacement 407 408 689 ''' 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) 413 707 414 708 def decorating_function(user_function): 415 416 stats = [0, 0 ]# make statistics updateable non-locally417 HIT S, MISSES = 0, 1# names for the stats fields709 #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 418 712 _len = len # localize the global len() function 419 713 #lock = RLock() # linkedlist updates aren't threadsafe 420 714 421 715 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 425 720 try: 721 # get cache entry 426 722 result = cache[key] 427 stats[HIT S] += 1723 stats[HIT] += 1 428 724 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 434 738 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())] 436 744 return result 437 745 438 def clear(): 746 def archive(obj): 747 """Replace the cache archive""" 748 cache.archive = obj 749 750 def clear(keepstats=False): 439 751 """Clear the cache and statistics""" 440 752 cache.clear() 441 stats[:] = [0, 0]753 if not keepstats: stats[:] = [0, 0, 0] 442 754 443 755 def info(): 444 756 """Report cache statistics""" 445 return _CacheInfo(stats[HIT S], stats[MISSES], maxsize, len(cache))757 return _CacheInfo(stats[HIT], stats[MISS], stats[LOAD], maxsize, len(cache)) 446 758 447 759 # interface … … 449 761 wrapper.info = info 450 762 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 452 768 #wrapper._queue = None #XXX 453 769 return update_wrapper(wrapper, user_function) … … 456 772 457 773 458 def _test_hits(algorithm, maxsize=20, keymap=None, rangelimit=5, tries=1000): 774 def _test_hits(algorithm, maxsize=20, keymap=None, 775 rangelimit=5, tries=1000, archived=False): 459 776 460 777 @algorithm(maxsize=maxsize, keymap=keymap) 461 778 def f(x, y): 462 779 return 3*x+y 780 781 if archived: 782 from archives import file_archive 783 f.archive(file_archive('cache.pkl')) 463 784 464 785 domain = range(rangelimit) … … 468 789 r = f(choice(domain), choice(domain)) 469 790 791 f.dump() 470 792 print (f.info()) 471 793 … … 473 795 if __name__ == '__main__': 474 796 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]: 476 799 print cache.__name__, ":", 477 800 _test_hits(cache, maxsize=1000, rangelimit=50) 478 801 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) 479 806 480 807 # EOF -
branches/decorate/memoize.py
r664 r665 3 3 """ 4 4 from keymaps import stringmap 5 from archives import archive_dict 6 from rounding import deep_round, simple_round 5 7 6 __all__ = ['memoize','memoized','archive_dict',\ 7 'null_archive','file_archive','db_archive'] 8 __all__ = ['memoize', 'memoized'] 8 9 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 ??? 415 12 416 13 def memoized(cache=None, keymap=None, tol=None, deep=False): … … 459 56 return dec 460 57 461 #FIXME: use cache maxsize algorithms... where dump to archive if maxsize462 58 463 59 class memoize(object): -
branches/decorate/rounding.py
r664 r665 1 1 """ 2 decorators that cache results to memory, to file, or to a database2 decorators that provide rounding 3 3 """ 4 from keymaps import stringmap5 4 6 __all__ = ['memoize','memoized','archive_dict',\ 7 'null_archive','file_archive','db_archive'] 5 __all__ = ['deep_round', 'shallow_round', 'simple_round'] 8 6 9 7 def isiterable(x): … … 117 115 118 116 119 #FIXME: the below need expiration of cache due to time, calls, etc...120 #FIXME: memoize*_round fails when decorating a class method121 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 backend126 127 Additional Inputs:128 archive: instance of archive object129 """130 self.__swap__ = null_archive()131 self.__archive__ = kwds.pop('archive', null_archive())132 dict.__init__(self, *args, **kwds)133 return134 def load(self, *args):135 """load archive contents136 137 If arguments are given, only load the specified keys138 """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 pass146 return147 def dump(self, *args):148 """dump contents to archive149 150 If arguments are given, only dump the specified keys151 """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 return158 def archived(self, *on):159 """check if the dict is archived, or toggle archiving160 161 If on is True, turn on the archive; if on is False, turn off the archive162 """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 # return179 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__ = archive184 # interface185 archive = property(__get_archive, __archive)186 pass187 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 return195 def __asdict__(self):196 return self197 def __setitem__(self, key, value):198 pass199 def update(self, adict, **kwds):200 pass201 def setdefault(self, key, *value):202 return self.get(key, *value)203 def __repr__(self):204 return "archive(NULL)"205 pass206 207 208 class file_archive(dict):209 """dictionary-style interface to a file"""210 def __init__(self, filename=None, serialized=True): # False211 """initialize a file with a synchronized dictionary interface212 213 Inputs:214 serialized: if True, pickle file contents; otherwise save python objects215 filename: name of the file backend [default: memo.pkl or memo.py]216 """217 import os218 """filename = full filepath"""219 if filename is None:220 if serialized: filename = 'memo.pkl' #FIXME: need better default221 else: filename = 'memo.py' #FIXME: need better default222 self._filename = filename223 self._serialized = serialized224 if not os.path.exists(filename):225 self.__save__({})226 return227 def __asdict__(self):228 if self._serialized:229 try:230 f = open(self._filename, 'rb')231 import dill as pickle232 memo = pickle.load(f)233 f.close()234 except:235 memo = {}236 else:237 import os238 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: unsafe245 os.chdir(curdir)246 return memo247 def __save__(self, memo=None):248 if memo == None: return249 if self._serialized:250 try:251 f = open(self._filename, 'wb')252 import dill as pickle253 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 return260 #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] = value271 self.__save__(memo)272 return273 def clear(self):274 self.__save__({})275 return276 #FIXME: copy, fromkeys277 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 res296 #FIXME: popitem297 def setdefault(self, key, *value):298 res = self.__asdict__().get(key, *value)299 self.__setitem__(key, res)300 return res301 def update(self, adict, **kwds):302 memo = self.__asdict__()303 memo.update(adict, **kwds)304 self.__save__(memo)305 return306 def values(self):307 return list(self.itervalues())308 pass309 310 311 #XXX: should inherit from object not dict ?312 class db_archive(dict): #XXX: requires UTF-8 key313 """dictionary-style interface to a database"""314 def __init__(self, database=None, table=None):315 """initialize a database with a synchronized dictionary interface316 317 Inputs:318 database: url of the database backend [default: :memory:]319 table: name of the associated database table320 """321 if database is None: database = ':memory:'322 self._database = database323 if table is None: table = 'memo'324 self._table = table325 import sqlite3 as db326 self._conn = db.connect(database)327 self._curs = self._conn.cursor()328 sql = "create table if not exists %s(argstr, fval)" % table329 self._curs.execute(sql)330 return331 def __asdict__(self):332 sql = "select * from %s" % self._table333 res = self._curs.execute(sql)334 d = {}335 [d.update({k:v}) for (k,v) in res] # always get the last one336 return d337 #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 one341 raise KeyError, key342 def __iter__(self):343 sql = "select argstr from %s" % self._table344 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 values348 sql = "insert into %s values(?,?)" % self._table349 self._curs.execute(sql, (key,value))350 self._conn.commit()351 return352 def clear(self):353 [self.pop(k) for k in self.keys()] # better delete table, add empty ?354 return355 #FIXME: copy, fromkeys356 def get(self, key, value=None):357 res = self._select_key_items(key)358 if res: value = res[-1][-1]359 return value360 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, key382 _value = value[0]383 sql = "delete from %s where argstr = ?" % self._table384 self._curs.execute(sql, (key,))385 self._conn.commit()386 return _value387 #FIXME: popitem388 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 = None397 else: _value = value[0]398 self.__setitem__(key, _value)399 return _value400 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 return405 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._table412 return tuple(self._curs.execute(sql, (key,)))413 pass414 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, and419 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 of421 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_round434 else: rounded = simple_round435 #else: rounded = shallow_round #FIXME: slow436 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 res455 except: #TypeError456 return f(*args, **kwds)457 func.cache = cache458 return func459 return dec460 461 #FIXME: use cache maxsize algorithms... where dump to archive if maxsize462 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, and466 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 = func471 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 = cache475 self.__keymap = keymap476 477 if deep: rounded = deep_round478 else: rounded = simple_round479 480 @rounded(tol)481 def rounded_args(*args, **kwds):482 return (args, kwds)483 self.__rounded_args = rounded_args484 return485 486 def __call__(self, func):487 self.func = func488 import functools489 @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] = res502 return res503 except: #TypeError504 return self.func(*args, **kwds)505 return dec506 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 functools514 return functools.partial(self.__call__, obj)515 516 117 # EOF -
branches/decorate/surrogate.py
r664 r665 20 20 21 21 22 from memoize import memoized, archive_dict, file_archive 23 from keymaps import picklemap 22 from memoize import memoized 23 from archives import archive_dict, file_archive 24 from keymaps import picklemap, hashmap 24 25 dumps = picklemap(flat=False) 25 26 cache = archive_dict(archive=file_archive('surrogate.pkl')) -
branches/decorate/test_cached_memoize.py
r664 r665 1 from memoize import memoized, file_archive 1 from memoize import memoized 2 from archives import file_archive 2 3 from timer import timed 3 4 -
branches/decorate/test_memoize.py
r664 r665 110 110 111 111 112 from memoizeimport archive_dict, db_archive112 from archives import archive_dict, db_archive 113 113 import dill 114 114 @memoized(cache=archive_dict(archive=db_archive()))
Note: See TracChangeset
for help on using the changeset viewer.