Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """This module provides a decorator to cache the results of a function. | 5 """This module provides a decorator to cache the results of a function. |
| 6 | 6 |
| 7 Examples: | 7 Examples: |
| 8 1. Decorate a function: | 8 1. Decorate a function: |
| 9 @cache_decorator.Cached() | 9 @cache_decorator.Cached() |
| 10 def Test(a): | 10 def Test(a): |
| (...skipping 23 matching lines...) Expand all Loading... | |
| 34 d2.Download('path') # Returned the cached downloaded data. | 34 d2.Download('path') # Returned the cached downloaded data. |
| 35 """ | 35 """ |
| 36 | 36 |
| 37 import functools | 37 import functools |
| 38 import hashlib | 38 import hashlib |
| 39 import logging | 39 import logging |
| 40 import inspect | 40 import inspect |
| 41 import pickle | 41 import pickle |
| 42 | 42 |
| 43 | 43 |
| 44 def _DefaultKeyGenerator(func, args, kwargs): | 44 def _DefaultKeyGenerator(func, args, kwargs, namespace=None): |
| 45 """Generates a key from the function and arguments passed to it. | 45 """Generates a key from the function and arguments passed to it. |
| 46 | 46 |
| 47 N.B. ``args`` and ``kwargs`` of function ``func`` should be pickleable, | |
| 48 or each of the parameter has an ``identifier`` property or method which | |
| 49 returns pickleable results. | |
| 50 | |
| 47 Args: | 51 Args: |
| 48 func (function): An arbitrary function. | 52 func (function): An arbitrary function. |
| 49 args (list): Positional arguments passed to ``func``. | 53 args (list): Positional arguments passed to ``func``. |
| 50 kwargs (dict): Keyword arguments passed to ``func``. | 54 kwargs (dict): Keyword arguments passed to ``func``. |
| 55 namespace (str): A prefix to the key for the cache. | |
| 51 | 56 |
| 52 Returns: | 57 Returns: |
| 53 A string to represent a call to the given function with the given arguments. | 58 A string to represent a call to the given function with the given arguments. |
| 54 """ | 59 """ |
| 55 params = inspect.getcallargs(func, *args, **kwargs) | 60 params = inspect.getcallargs(func, *args, **kwargs) |
| 56 for var_name in params: | 61 for var_name in params: |
| 57 if not hasattr(params[var_name], 'identifier'): | 62 if not hasattr(params[var_name], 'identifier'): |
| 58 continue | 63 continue |
| 59 | 64 |
| 60 if callable(params[var_name].identifier): | 65 if callable(params[var_name].identifier): |
| 61 params[var_name] = params[var_name].identifier() | 66 params[var_name] = params[var_name].identifier() |
| 62 else: | 67 else: |
| 63 params[var_name] = params[var_name].identifier | 68 params[var_name] = params[var_name].identifier |
| 64 | 69 |
| 65 return hashlib.md5(pickle.dumps(params)).hexdigest() | 70 encoded_params = hashlib.md5(pickle.dumps(params)).hexdigest() |
| 71 prefix = namespace or '%s.%s' % (func.__module__, func.__name__) | |
| 72 | |
| 73 return '%s-%s' % (prefix, encoded_params) | |
| 66 | 74 |
| 67 | 75 |
| 68 def Cached(namespace=None, | 76 class CacheDecorator(object): |
| 69 expire_time=0, | 77 """Abstract decorator class to cache the decorated function. |
| 70 key_generator=_DefaultKeyGenerator, | |
| 71 cache=None): | |
| 72 """Returns a decorator to cache the decorated function's results. | |
| 73 | |
| 74 However, if the function returns None, empty list/dict, empty string, or other | |
| 75 value that is evaluated as False, the results won't be cached. | |
| 76 | |
| 77 This decorator is to cache results of different calls to the decorated | |
| 78 function, and avoid executing it again if the calls are equivalent. Two calls | |
| 79 are equivalent, if the namespace is the same and the keys generated by the | |
| 80 ``key_generator`` are the same. | |
| 81 | 78 |
| 82 The usage of this decorator requires that: | 79 The usage of this decorator requires that: |
| 83 - If the default key generator is used, parameters passed to the decorated | 80 - If the default key generator is used, parameters passed to the decorated |
| 84 function should be pickleable, or each of the parameter has an identifier | 81 function should be pickleable, or each of the parameter has an identifier |
| 85 property or method which returns pickleable results. | 82 property or method which returns pickleable results. |
| 86 - If the default cache is used, the returned results of the decorated | 83 - If the default cache is used, the returned results of the decorated |
| 87 function should be pickleable. | 84 function should be pickleable. |
| 85 """ | |
| 88 | 86 |
| 89 Args: | 87 def __init__(self, cache, namespace=None, expire_time=0, |
| 90 namespace (str): A prefix to the key for the cache. Default to the | 88 key_generator=_DefaultKeyGenerator): |
| 89 """ | |
| 90 Args: | |
| 91 cache (Cache): An instance of an implementation of interface `Cache`. | |
| 92 Defaults to None. | |
| 93 namespace (str): A prefix to the key for the cache. Default to the | |
| 91 combination of module name and function name of the decorated function. | 94 combination of module name and function name of the decorated function. |
| 92 expire_time (int): Expiration time, relative number of seconds from current | 95 expire_time (int): Expiration time, relative number of seconds from |
| 93 time (up to 0 month). Defaults to 0 -- never expire. | 96 current time (up to 0 month). Defaults to 0 -- never expire. |
| 94 key_generator (function): A function to generate a key to represent a call | 97 key_generator (function): A function to generate a key to represent a call |
| 95 to the decorated function. Defaults to :func:`_DefaultKeyGenerator`. | 98 to the decorated function. Defaults to :func:`_DefaultKeyGenerator`. |
| 96 cache (Cache): An instance of an implementation of interface `Cache`. | 99 """ |
| 97 Defaults to None. | 100 self._cache = cache |
| 101 self._namespace = namespace | |
| 102 self._expire_time = expire_time | |
| 103 self._key_generator = key_generator | |
| 98 | 104 |
| 99 Returns: | 105 def GetCache(self, key, func, args, kwargs): |
| 100 The cached results or the results of a new run of the decorated function. | 106 """Gets cached result for key. |
| 107 | |
| 108 Args: | |
| 109 key (str): The key to retriev result from. | |
| 110 func (callable): The function to decorate. | |
| 111 args (iterable): The argument list of the decorated function. | |
| 112 args (dict): The keyword arguments of the decorated function. | |
|
chanli
2017/01/21 07:11:56
Nit: kwargs
| |
| 113 | |
| 114 Returns: | |
| 115 Returns cached result of key from the ``Cache`` instance. | |
| 116 N.B. If there is Exception retrieving the cached result, the returned | |
| 117 result will be set to None. | |
| 118 """ | |
| 119 try: | |
| 120 result = self._cache.Get(key) | |
| 121 except Exception: # pragma: no cover. | |
| 122 result = None | |
| 123 logging.exception( | |
| 124 'Failed to get cached data for function %s.%s, args=%s, kwargs=%s', | |
| 125 func.__module__, func.__name__, repr(args), repr(kwargs)) | |
| 126 | |
| 127 return result | |
| 128 | |
| 129 def SetCache(self, key, result, func, args, kwargs): | |
| 130 """Sets result to ``self._cache``. | |
| 131 | |
| 132 Args: | |
| 133 key (str): The key to retriev result from. | |
| 134 result (any type): The result of the key. | |
| 135 func (callable): The function to decorate. | |
| 136 args (iterable): The argument list of the decorated function. | |
| 137 args (dict): The keyword arguments of the decorated function. | |
|
chanli
2017/01/21 07:11:56
Nit: kwargs
| |
| 138 | |
| 139 Returns: | |
| 140 Boolean indicating if ``result`` was successfully set or not. | |
| 141 """ | |
| 142 try: | |
| 143 self._cache.Set(key, result, expire_time=self._expire_time) | |
| 144 except Exception: # pragma: no cover. | |
| 145 logging.exception( | |
| 146 'Failed to cache data for function %s.%s, args=%s, kwargs=%s', | |
| 147 func.__module__, func.__name__, repr(args), repr(kwargs)) | |
| 148 return False | |
| 149 | |
| 150 return True | |
| 151 | |
| 152 def __call__(self, func): | |
| 153 """Returns a wrapped function of ``func`` which utilize ``self._cache``.""" | |
| 154 raise NotImplementedError() | |
| 155 | |
| 156 | |
| 157 class Cached(CacheDecorator): | |
| 158 """Decorator to cache function's results. | |
| 159 | |
| 160 N.B. the decorated function should have return values, because if the function | |
| 161 returns None, empty list/dict, empty string, or other value that is evaluated | |
| 162 as False, the results won't be cached. | |
| 163 | |
| 164 This decorator is to cache results of different calls to the decorated | |
| 165 function, and avoid executing it again if the calls are equivalent. Two calls | |
| 166 are equivalent, if the namespace is the same and the keys generated by the | |
| 167 ``key_generator`` are the same. | |
| 101 """ | 168 """ |
| 102 def GetPrefix(func, namespace): | |
| 103 return namespace or '%s.%s' % (func.__module__, func.__name__) | |
| 104 | 169 |
| 105 def Decorator(func): | 170 def __call__(self, func): |
| 106 """Decorator to cache a function's results.""" | 171 """Decorator to cache a function's results.""" |
| 107 @functools.wraps(func) | 172 @functools.wraps(func) |
| 108 def Wrapped(*args, **kwargs): | 173 def Wrapped(*args, **kwargs): |
| 109 prefix = GetPrefix(func, namespace) | 174 key = self._key_generator(func, args, kwargs, namespace=self._namespace) |
| 110 key = '%s-%s' % (prefix, key_generator(func, args, kwargs)) | 175 cached_result = self.GetCache(key, func, args, kwargs) |
| 111 | 176 |
| 112 try: | 177 if cached_result is not None: |
| 113 result = cache.Get(key) | 178 return cached_result |
| 114 except Exception: # pragma: no cover. | |
| 115 result = None | |
| 116 logging.exception( | |
| 117 'Failed to get cached data for function %s.%s, args=%s, kwargs=%s', | |
| 118 func.__module__, func.__name__, repr(args), repr(kwargs)) | |
| 119 | |
| 120 if result is not None: | |
| 121 return result | |
| 122 | 179 |
| 123 result = func(*args, **kwargs) | 180 result = func(*args, **kwargs) |
| 124 if result: | 181 if result: |
| 125 try: | 182 self.SetCache(key, result, func, args, kwargs) |
| 126 cache.Set(key, result, expire_time=expire_time) | |
| 127 except Exception: # pragma: no cover. | |
| 128 logging.exception( | |
| 129 'Failed to cache data for function %s.%s, args=%s, kwargs=%s', | |
| 130 func.__module__, func.__name__, repr(args), repr(kwargs)) | |
| 131 | 183 |
| 132 return result | 184 return result |
| 133 | 185 |
| 134 return Wrapped | 186 return Wrapped |
| 135 | 187 |
| 136 return Decorator | 188 |
| 189 class GeneratorCached(CacheDecorator): | |
| 190 """Decorator to cache a generator function. | |
| 191 | |
| 192 N.B. the decorated function must be a generator which ``yield``s results. | |
| 193 | |
| 194 The key of the generator function will map to a list of sub-keys mapping to | |
| 195 each element result.N.B. All the results must be cached, no matter it's | |
| 196 empty or not. If any result failed to be cached, the whole caching is | |
| 197 considered failed and the key to the generator must NOT be set. | |
| 198 | |
| 199 This decorator is to cache all results of the generator, and ``yield`` them | |
| 200 one by one from ``self._cache``. | |
| 201 """ | |
| 202 | |
| 203 def __call__(self, func): | |
| 204 """Decorator to cache a generator function.""" | |
| 205 @functools.wraps(func) | |
| 206 def Wrapped(*args, **kwargs): | |
| 207 key = self._key_generator(func, args, kwargs, namespace=self._namespace) | |
| 208 cached_keys = self.GetCache(key, func, args, kwargs) | |
| 209 | |
| 210 if cached_keys is not None: | |
| 211 for cached_key in cached_keys: | |
| 212 yield self.GetCache(cached_key, func, args, kwargs) | |
| 213 | |
| 214 result_iter = func(*args, **kwargs) | |
| 215 result_keys = [] | |
| 216 | |
| 217 cache_success = True | |
| 218 for index, result in enumerate(result_iter): | |
| 219 yield result | |
| 220 result_key = '%s-%d' % (key, index) | |
| 221 if not cache_success or not self.SetCache(result_key, result, | |
| 222 func, args, kwargs): | |
| 223 cache_success = False | |
| 224 continue | |
| 225 | |
| 226 result_keys.append(result_key) | |
| 227 | |
| 228 if cache_success: | |
| 229 self.SetCache(key, result_keys, func, args, kwargs) | |
| 230 | |
| 231 return Wrapped | |
| OLD | NEW |