Package paramz :: Module caching
[hide private]
[frames] | no frames]

Source Code for Module paramz.caching

  1  #=============================================================================== 
  2  # Copyright (c) 2014, GPy authors (see AUTHORS.txt). 
  3  # Copyright (c) 2014, James Hensman, Max Zwiessele, Zhenwen Dai 
  4  # Copyright (c) 2015, Max Zwiessele 
  5  # 
  6  # All rights reserved. 
  7  # 
  8  # Redistribution and use in source and binary forms, with or without 
  9  # modification, are permitted provided that the following conditions are met: 
 10  # 
 11  # * Redistributions of source code must retain the above copyright notice, this 
 12  #   list of conditions and the following disclaimer. 
 13  # 
 14  # * Redistributions in binary form must reproduce the above copyright notice, 
 15  #   this list of conditions and the following disclaimer in the documentation 
 16  #   and/or other materials provided with the distribution. 
 17  # 
 18  # * Neither the name of paramax nor the names of its 
 19  #   contributors may be used to endorse or promote products derived from 
 20  #   this software without specific prior written permission. 
 21  # 
 22  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 23  # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 24  # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 25  # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
 26  # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 27  # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 28  # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 29  # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 30  # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 31  # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 32  #=============================================================================== 
 33   
 34  import collections, weakref 
 35  from functools import reduce 
 36  from pickle import PickleError 
 37  from decorator import decorate  # @UnresolvedImport 
 38   
 39  from .core.observable import Observable 
 40  from numbers import Number 
41 42 -class Cacher(object):
43 - def __init__(self, operation, limit=3, ignore_args=(), force_kwargs=(), cacher_enabled=True):
44 """ 45 Cache an `operation`. If the operation is a bound method we will 46 create a cache (FunctionCache) on that object in order to keep track 47 of the caches on instances. 48 49 Warning: If the instance already had a Cacher for the operation 50 that Cacher will be overwritten by this Cacher! 51 52 :param callable operation: function to cache 53 :param int limit: depth of cacher 54 :param [int] ignore_args: list of indices, pointing at arguments to ignore in `*args` of `operation(*args)`. This includes self, so make sure to ignore self, if it is not cachable and you do not want this to prevent caching! 55 :param [str] force_kwargs: list of kwarg names (strings). If a kwarg with that name is given, the cacher will force recompute and wont cache anything. 56 :param int verbose: verbosity level. 0: no print outs, 1: casual print outs, 2: debug level print outs 57 """ 58 self.limit = int(limit) 59 self.ignore_args = ignore_args 60 self.force_kwargs = force_kwargs 61 self.operation = operation 62 self.cacher_enabled = cacher_enabled # Is the caching switched on, for this chacher? 63 if getattr(self.operation, '__self__', None) is not None: 64 obj = self.operation.__self__ 65 if not hasattr(obj, 'cache'): 66 obj.cache = FunctionCache() 67 cache = obj.cache 68 self.cacher_enabled = (self.cacher_enabled and cache.caching_enabled) 69 # if the cache of the object is enabled, we want this cacher to 70 # be enabled, if not told explicitly to not be. 71 cache[self.operation] = self 72 73 self.cached_input_ids = {} # make sure reset can be run and we do not duplicate 74 # code. See reset for details on saved values. 75 self.reset()
76
77 - def disable_cacher(self):
78 "Disable the caching of this cacher. This also removes previously cached results" 79 self.cacher_enabled = False 80 self.reset()
81
82 - def enable_cacher(self):
83 "Enable the caching of this cacher." 84 self.cacher_enabled = True
85
86 - def id(self, obj):
87 """returns the self.id of an object, to be used in caching individual self.ids""" 88 return hex(id(obj))
89
90 - def combine_inputs(self, args, kw, ignore_args):
91 "Combines the args and kw in a unique way, such that ordering of kwargs does not lead to recompute" 92 inputs= args + tuple(c[1] for c in sorted(kw.items(), key=lambda x: x[0])) 93 # REMOVE the ignored arguments from input and PREVENT it from being checked!!! 94 return [a for i,a in enumerate(inputs) if i not in ignore_args]
95
96 - def prepare_cache_id(self, combined_args_kw):
97 "get the cacheid (conc. string of argument self.ids in order)" 98 cache_id = "".join(self.id(a) for a in combined_args_kw) 99 return cache_id
100
101 - def ensure_cache_length(self):
102 "Ensures the cache is within its limits and has one place free" 103 if len(self.order) == self.limit: 104 # we have reached the limit, so lets release one element 105 cache_id = self.order.popleft() 106 combined_args_kw = self.cached_inputs[cache_id] 107 for ind in combined_args_kw: 108 ind_id = self.id(ind) 109 tmp = self.cached_input_ids.get(ind_id, None) 110 if tmp is not None: 111 ref, cache_ids = tmp 112 if len(cache_ids) == 1 and ref() is not None: 113 ref().remove_observer(self, self.on_cache_changed) 114 del self.cached_input_ids[ind_id] 115 else: 116 cache_ids.remove(cache_id) 117 self.cached_input_ids[ind_id] = [ref, cache_ids] 118 try: 119 del self.cached_outputs[cache_id] 120 except KeyError: 121 # Was not cached before, possibly a keyboard interrupt 122 pass 123 try: 124 del self.inputs_changed[cache_id] 125 except KeyError: 126 # Was not cached before, possibly a keyboard interrupt 127 pass 128 try: 129 del self.cached_inputs[cache_id] 130 except KeyError: 131 # Was not cached before, possibly a keyboard interrupt 132 pass
133
134 - def add_to_cache(self, cache_id, inputs, output):
135 """This adds cache_id to the cache, with inputs and output""" 136 self.inputs_changed[cache_id] = False 137 self.cached_outputs[cache_id] = output 138 self.order.append(cache_id) 139 self.cached_inputs[cache_id] = inputs 140 for a in inputs: 141 if a is not None and not isinstance(a, Number) and not isinstance(a, str): 142 ind_id = self.id(a) 143 v = self.cached_input_ids.get(ind_id, [weakref.ref(a), []]) 144 v[1].append(cache_id) 145 if len(v[1]) == 1: 146 a.add_observer(self, self.on_cache_changed) 147 self.cached_input_ids[ind_id] = v
148
149 - def __call__(self, *args, **kw):
150 """ 151 A wrapper function for self.operation, 152 """ 153 #======================================================================= 154 # !WARNING CACHE OFFSWITCH! 155 if not self.cacher_enabled: 156 return self.operation(*args, **kw) 157 #======================================================================= 158 159 # 1: Check whether we have forced recompute arguments: 160 if len(self.force_kwargs) != 0: 161 for k in self.force_kwargs: 162 if k in kw and kw[k] is not None: 163 return self.operation(*args, **kw) 164 165 # 2: prepare_cache_id and get the unique self.id string for this call 166 inputs = self.combine_inputs(args, kw, self.ignore_args) 167 cache_id = self.prepare_cache_id(inputs) 168 # 2: if anything is not cachable, we will just return the operation, without caching 169 if reduce(lambda a, b: a or (not (isinstance(b, Observable) or b is None or isinstance(b, Number) or isinstance(b, str))), inputs, False): 170 # print 'WARNING: '+self.operation.__name__ + ' not cacheable!' 171 # print [not (isinstance(b, Observable)) for b in inputs] 172 return self.operation(*args, **kw) 173 # 3&4: check whether this cache_id has been cached, then has it changed? 174 not_seen = not(cache_id in self.inputs_changed) 175 changed = (not not_seen) and self.inputs_changed[cache_id] 176 if changed or not_seen: 177 # If we need to compute, we compute the operation, but fail gracefully, if the operation has an error: 178 try: 179 new_output = self.operation(*args, **kw) 180 except: 181 self.reset() 182 raise 183 if(changed): 184 # 4: This happens, when elements have changed for this cache self.id 185 self.inputs_changed[cache_id] = False 186 self.cached_outputs[cache_id] = new_output 187 else: # not_seen is True, as one of the two has to be True: 188 # 3: This is when we never saw this chache_id: 189 self.ensure_cache_length() 190 self.add_to_cache(cache_id, inputs, new_output) 191 # 5: We have seen this cache_id and it is cached: 192 return self.cached_outputs[cache_id]
193
194 - def on_cache_changed(self, direct, which=None):
195 """ 196 A callback funtion, which sets local flags when the elements of some cached inputs change 197 198 this function gets 'hooked up' to the inputs when we cache them, and upon their elements being changed we update here. 199 """ 200 for what in [direct, which]: 201 ind_id = self.id(what) 202 _, cache_ids = self.cached_input_ids.get(ind_id, [None, []]) 203 for cache_id in cache_ids: 204 self.inputs_changed[cache_id] = True
205
206 - def reset(self):
207 """ 208 Totally reset the cache 209 """ 210 [a().remove_observer(self, self.on_cache_changed) if (a() is not None) else None for [a, _] in self.cached_input_ids.values()] 211 212 self.order = collections.deque() 213 self.cached_inputs = {} # point from cache_ids to a list of [ind_ids], which where used in cache cache_id 214 215 #======================================================================= 216 # point from each ind_id to [ref(obj), cache_ids] 217 # 0: a weak reference to the object itself 218 # 1: the cache_ids in which this ind_id is used (len will be how many times we have seen this ind_id) 219 self.cached_input_ids = {} 220 #======================================================================= 221 222 self.cached_outputs = {} # point from cache_ids to outputs 223 self.inputs_changed = {} # point from cache_ids to bools
224
225 - def __deepcopy__(self, memo=None):
226 return Cacher(self.operation, self.limit, self.ignore_args, self.force_kwargs)
227
228 - def __getstate__(self, memo=None):
229 raise PickleError("Trying to pickle Cacher object with function {}, pickling functions not possible.".format(str(self.operation)))
230
231 - def __setstate__(self, memo=None):
232 raise PickleError("Trying to pickle Cacher object with function {}, pickling functions not possible.".format(str(self.operation)))
233 234 @property
235 - def __name__(self):
236 return self.operation.__name__
237
238 - def __str__(self, *args, **kwargs):
239 return "Cacher({})\n limit={}\n \#cached={}".format(self.__name__, self.limit, len(self.cached_input_ids))
240
241 -class FunctionCache(dict):
242 - def __init__(self, *args, **kwargs):
243 dict.__init__(self, *args, **kwargs) 244 self.caching_enabled = True
245
246 - def disable_caching(self):
247 "Disable the cache of this object. This also removes previously cached results" 248 self.caching_enabled = False 249 for c in self.values(): 250 c.disable_cacher()
251
252 - def enable_caching(self):
253 "Enable the cache of this object." 254 self.caching_enabled = True 255 for c in self.values(): 256 c.enable_cacher()
257
258 - def reset(self):
259 "Reset (delete) the cache of this object" 260 for c in self.values(): 261 c.reset()
262
263 -class Cache_this(object):
264 """ 265 A decorator which can be applied to bound methods in order to cache them 266 """
267 - def __init__(self, limit=5, ignore_args=(), force_kwargs=()):
268 self.limit = limit 269 self.ignore_args = ignore_args 270 self.force_kwargs = force_kwargs 271 self.f = None
272 - def __call__(self, f):
273 self.f = f 274 def g(obj, *args, **kw): 275 obj = args[0] 276 if not hasattr(obj, 'cache'): 277 obj.cache = FunctionCache() 278 cache = obj.cache 279 if self.f in cache: 280 cacher = cache[self.f] 281 else: 282 cacher = cache[self.f] = Cacher(self.f, self.limit, self.ignore_args, self.force_kwargs, cacher_enabled=cache.caching_enabled) 283 return cacher(*args, **kw)
284 g.__name__ = f.__name__ 285 g.__doc__ = f.__doc__ 286 g = decorate(self.f, g) 287 return g
288