|
1
|
|
|
""" |
|
2
|
|
|
Tools for memoization of function results. |
|
3
|
|
|
""" |
|
4
|
|
|
from functools import wraps |
|
5
|
|
|
from six import iteritems |
|
6
|
|
|
from weakref import WeakKeyDictionary |
|
7
|
|
|
|
|
8
|
|
|
|
|
9
|
|
|
class lazyval(object): |
|
10
|
|
|
"""Decorator that marks that an attribute of an instance should not be |
|
11
|
|
|
computed until needed, and that the value should be memoized. |
|
12
|
|
|
|
|
13
|
|
|
Example |
|
14
|
|
|
------- |
|
15
|
|
|
|
|
16
|
|
|
>>> from zipline.utils.memoize import lazyval |
|
17
|
|
|
>>> class C(object): |
|
18
|
|
|
... def __init__(self): |
|
19
|
|
|
... self.count = 0 |
|
20
|
|
|
... @lazyval |
|
21
|
|
|
... def val(self): |
|
22
|
|
|
... self.count += 1 |
|
23
|
|
|
... return "val" |
|
24
|
|
|
... |
|
25
|
|
|
>>> c = C() |
|
26
|
|
|
>>> c.count |
|
27
|
|
|
0 |
|
28
|
|
|
>>> c.val, c.count |
|
29
|
|
|
('val', 1) |
|
30
|
|
|
>>> c.val, c.count |
|
31
|
|
|
('val', 1) |
|
32
|
|
|
>>> c.val = 'not_val' |
|
33
|
|
|
Traceback (most recent call last): |
|
34
|
|
|
... |
|
35
|
|
|
AttributeError: Can't set read-only attribute. |
|
36
|
|
|
>>> c.val |
|
37
|
|
|
'val' |
|
38
|
|
|
""" |
|
39
|
|
|
def __init__(self, get): |
|
40
|
|
|
self._get = get |
|
41
|
|
|
self._cache = WeakKeyDictionary() |
|
42
|
|
|
|
|
43
|
|
|
def __get__(self, instance, owner): |
|
44
|
|
|
if instance is None: |
|
45
|
|
|
return self |
|
46
|
|
|
try: |
|
47
|
|
|
return self._cache[instance] |
|
48
|
|
|
except KeyError: |
|
49
|
|
|
self._cache[instance] = val = self._get(instance) |
|
50
|
|
|
return val |
|
51
|
|
|
|
|
52
|
|
|
def __set__(self, instance, value): |
|
53
|
|
|
raise AttributeError("Can't set read-only attribute.") |
|
54
|
|
|
|
|
55
|
|
|
def __delitem__(self, instance): |
|
56
|
|
|
del self._cache[instance] |
|
57
|
|
|
|
|
58
|
|
|
|
|
59
|
|
|
class classlazyval(lazyval): |
|
60
|
|
|
""" Decorator that marks that an attribute of a class should not be |
|
61
|
|
|
computed until needed, and that the value should be memoized. |
|
62
|
|
|
|
|
63
|
|
|
Example |
|
64
|
|
|
------- |
|
65
|
|
|
|
|
66
|
|
|
>>> from zipline.utils.memoize import classlazyval |
|
67
|
|
|
>>> class C(object): |
|
68
|
|
|
... count = 0 |
|
69
|
|
|
... @classlazyval |
|
70
|
|
|
... def val(cls): |
|
71
|
|
|
... cls.count += 1 |
|
72
|
|
|
... return "val" |
|
73
|
|
|
... |
|
74
|
|
|
>>> C.count |
|
75
|
|
|
0 |
|
76
|
|
|
>>> C.val, C.count |
|
77
|
|
|
('val', 1) |
|
78
|
|
|
>>> C.val, C.count |
|
79
|
|
|
('val', 1) |
|
80
|
|
|
""" |
|
81
|
|
|
# We don't reassign the name on the class to implement the caching because |
|
82
|
|
|
# then we would need to use a metaclass to track the name of the |
|
83
|
|
|
# descriptor. |
|
84
|
|
|
def __get__(self, instance, owner): |
|
85
|
|
|
return super(classlazyval, self).__get__(owner, owner) |
|
86
|
|
|
|
|
87
|
|
|
|
|
88
|
|
|
def remember_last(f): |
|
89
|
|
|
""" |
|
90
|
|
|
Decorator that remembers the last computed value of a function and doesn't |
|
91
|
|
|
recompute it when called with the same inputs multiple times. |
|
92
|
|
|
|
|
93
|
|
|
Parameters |
|
94
|
|
|
---------- |
|
95
|
|
|
f : The function to be memoized. All arguments to f should be hashable. |
|
96
|
|
|
|
|
97
|
|
|
Example |
|
98
|
|
|
------- |
|
99
|
|
|
>>> counter = 0 |
|
100
|
|
|
>>> @remember_last |
|
101
|
|
|
... def foo(x): |
|
102
|
|
|
... global counter |
|
103
|
|
|
... counter += 1 |
|
104
|
|
|
... return x, counter |
|
105
|
|
|
>>> foo(1) |
|
106
|
|
|
(1, 1) |
|
107
|
|
|
>>> foo(1) |
|
108
|
|
|
(1, 1) |
|
109
|
|
|
>>> foo(0) |
|
110
|
|
|
(0, 2) |
|
111
|
|
|
>>> foo(1) |
|
112
|
|
|
(1, 3) |
|
113
|
|
|
|
|
114
|
|
|
Notes |
|
115
|
|
|
----- |
|
116
|
|
|
This decorator is equivalent to `lru_cache(1)` in Python 3, but with less |
|
117
|
|
|
bells and whistles for handling things like threadsafety. If we ever |
|
118
|
|
|
decide we need such bells and whistles, we should just make functools32 a |
|
119
|
|
|
dependency. |
|
120
|
|
|
""" |
|
121
|
|
|
# This needs to be a mutable data structure so we can change it from inside |
|
122
|
|
|
# the function. In pure Python 3, we'd use the nonlocal keyword for this. |
|
123
|
|
|
_previous = [None, None] |
|
124
|
|
|
KEY, VALUE = 0, 1 |
|
125
|
|
|
|
|
126
|
|
|
_kwd_mark = object() |
|
127
|
|
|
|
|
128
|
|
|
@wraps(f) |
|
129
|
|
|
def memoized_f(*args, **kwds): |
|
130
|
|
|
# Hashing logic taken from functools32.lru_cache. |
|
131
|
|
|
key = args |
|
132
|
|
|
if kwds: |
|
133
|
|
|
key += _kwd_mark + tuple(sorted(iteritems(kwds))) |
|
134
|
|
|
|
|
135
|
|
|
key_hash = hash(key) |
|
136
|
|
|
if key_hash != _previous[KEY]: |
|
137
|
|
|
_previous[VALUE] = f(*args, **kwds) |
|
138
|
|
|
_previous[KEY] = key_hash |
|
139
|
|
|
return _previous[VALUE] |
|
140
|
|
|
|
|
141
|
|
|
return memoized_f |
|
142
|
|
|
|