forked from grantjenks/python-diskcache
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmemo.py
More file actions
181 lines (140 loc) · 6.11 KB
/
memo.py
File metadata and controls
181 lines (140 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""Memoization utilities.
"""
from functools import wraps
from math import log
from random import random
from time import time
MARK = object()
def full_name(func):
"Return full name of `func` by adding the module and function name."
try:
# The __qualname__ attribute is only available in Python 3.3 and later.
# GrantJ 2019-03-29 Remove after support for Python 2 is dropped.
name = func.__qualname__
except AttributeError:
name = func.__name__
return func.__module__ + '.' + name
def _args_to_key(base, args, kwargs, typed):
"""Create cache key out of function arguments.
:param tuple base: base of key
:param tuple args: function arguments
:param dict kwargs: function keyword arguments
:param bool typed: include types in cache key
:return: cache key tuple
"""
key = base + args
if kwargs:
key += (MARK,)
sorted_items = sorted(kwargs.items())
for item in sorted_items:
key += item
if typed:
key += tuple(type(arg) for arg in args)
if kwargs:
key += tuple(type(value) for _, value in sorted_items)
return key
def memoize(cache, name=None, typed=False, expire=None, tag=None,
early_recompute=False, time_func=time):
"""Memoizing cache decorator.
Decorator to wrap callable with memoizing function using cache. Repeated
calls with the same arguments will lookup result in cache and avoid
function evaluation.
If name is set to None (default), the callable name will be determined
automatically.
If typed is set to True, function arguments of different types will be
cached separately. For example, f(3) and f(3.0) will be treated as distinct
calls with distinct results.
Cache stampedes are a type of cascading failure that can occur when
parallel computing systems using memoization come under heavy load. This
behaviour is sometimes also called dog-piling, cache miss storm, cache
choking, or the thundering herd problem.
The memoization decorator includes cache stampede protection through the
early recomputation parameter. When set to True (default False), the expire
parameter must not be None. Early recomputation of results will occur
probabilistically before expiration.
Early probabilistic recomputation is based on research by Vattani, A.;
Chierichetti, F.; Lowenstein, K. (2015), Optimal Probabilistic Cache
Stampede Prevention, VLDB, pp. 886?897, ISSN 2150-8097
The original underlying function is accessible through the __wrapped__
attribute. This is useful for introspection, for bypassing the cache, or
for rewrapping the function with a different cache.
>>> from diskcache import FanoutCache
>>> cache = FanoutCache()
>>> @cache.memoize(typed=True, expire=1, tag='fib')
... def fibonacci(number):
... if number == 0:
... return 0
... elif number == 1:
... return 1
... else:
... return fibonacci(number - 1) + fibonacci(number - 2)
>>> fibonacci(100)
354224848179261915075
An additional `__cache_key__` attribute can be used to generate the cache key
used for the given arguments.
>>> key = fibonacci.__cache_key__(100)
>>> cache[key]
354224848179261915075
Remember to call memoize when decorating a callable. If you forget, then a
TypeError will occur. Note the lack of parenthenses after memoize below:
>>> @cache.memoize
... def test():
... pass
Traceback (most recent call last):
...
TypeError: name cannot be callable
:param cache: cache to store callable arguments and return values
:param str name: name given for callable (default None, automatic)
:param bool typed: cache different types separately (default False)
:param float expire: seconds until arguments expire
(default None, no expiry)
:param str tag: text to associate with arguments (default None)
:param bool early_recompute: probabilistic early recomputation
(default False)
:param time_func: callable for calculating current time
:return: callable decorator
"""
# Caution: Nearly identical code exists in DjangoCache.memoize
if callable(name):
raise TypeError('name cannot be callable')
if early_recompute and expire is None:
raise ValueError('expire required')
def decorator(func):
"Decorator created by memoize call for callable."
base = (full_name(func),) if name is None else (name,)
if early_recompute:
@wraps(func)
def wrapper(*args, **kwargs):
"Wrapper for callable to cache arguments and return values."
key = wrapper.__cache_key__(*args, **kwargs)
pair, expire_time = cache.get(
key, default=MARK, expire_time=True, retry=True,
)
if pair is not MARK:
result, delta = pair
now = time_func()
ttl = expire_time - now
if (-delta * early_recompute * log(random())) < ttl:
return result
start = time_func()
result = func(*args, **kwargs)
delta = time_func() - start
pair = result, delta
cache.set(key, pair, expire=expire, tag=tag, retry=True)
return result
else:
@wraps(func)
def wrapper(*args, **kwargs):
"Wrapper for callable to cache arguments and return values."
key = wrapper.__cache_key__(*args, **kwargs)
result = cache.get(key, default=MARK, retry=True)
if result is MARK:
result = func(*args, **kwargs)
cache.set(key, result, expire=expire, tag=tag, retry=True)
return result
def __cache_key__(*args, **kwargs):
"Make key for cache given function arguments."
return _args_to_key(base, args, kwargs, typed)
wrapper.__cache_key__ = __cache_key__
return wrapper
return decorator