Completed
Pull Request — develop (#13)
by Kale
01:11
created

memoizeproperty()   B

Complexity

Conditions 3

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 36
rs 8.8571
1
from __future__ import absolute_import, division, print_function
2
from collections import Hashable
3
from types import GeneratorType
4
5
from ._vendor.six import wraps
6
7
# TODO: spend time filling out functionality and make these more robust
8
9
10
def memoize(func):
11
    """
12
    Decorator to cause a function to cache it's results for each combination of
13
    inputs and return the cached result on subsequent calls.  Does not support
14
    named arguments or arg values that are not hashable.
15
16
    >>> @memoize
17
    ... def foo(x):
18
    ...     print('running function with', x)
19
    ...     return x+3
20
    ...
21
    >>> foo(10)
22
    running function with 10
23
    13
24
    >>> foo(10)
25
    13
26
    >>> foo(11)
27
    running function with 11
28
    14
29
    >>> @memoize
30
    ... def range_tuple(limit):
31
    ...     print('running function')
32
    ...     return tuple(i for i in range(limit))
33
    ...
34
    >>> range_tuple(3)
35
    running function
36
    (0, 1, 2)
37
    >>> range_tuple(3)
38
    (0, 1, 2)
39
    >>> @memoize
40
    ... def range_iter(limit):
41
    ...     print('running function')
42
    ...     return (i for i in range(limit))
43
    ...
44
    >>> range_iter(3)
45
    Traceback (most recent call last):
46
    TypeError: Can't memoize a generator or non-hashable object!
47
    """
48
    func._result_cache = {}  # pylint: disable-msg=W0212
49
50
    @wraps(func)
51
    def _memoized_func(*args, **kwargs):
52
        key = (args, tuple(sorted(kwargs.items())))
53
        if key in func._result_cache:  # pylint: disable-msg=W0212
54
            return func._result_cache[key]  # pylint: disable-msg=W0212
55
        else:
56
            result = func(*args, **kwargs)
57
            if isinstance(result, GeneratorType) or not isinstance(result, Hashable):
58
                raise TypeError("Can't memoize a generator or non-hashable object!")
59
            func._result_cache[key] = result  # pylint: disable-msg=W0212
60
            return result
61
62
    return _memoized_func
63
64
65
def memoizemethod(method):
66
    """
67
    Decorator to cause a method to cache it's results in self for each
68
    combination of inputs and return the cached result on subsequent calls.
69
    Does not support named arguments or arg values that are not hashable.
70
71
    >>> class Foo (object):
72
    ...   @memoizemethod
73
    ...   def foo(self, x, y=0):
74
    ...     print('running method with', x, y)
75
    ...     return x + y + 3
76
    ...
77
    >>> foo1 = Foo()
78
    >>> foo2 = Foo()
79
    >>> foo1.foo(10)
80
    running method with 10 0
81
    13
82
    >>> foo1.foo(10)
83
    13
84
    >>> foo2.foo(11, y=7)
85
    running method with 11 7
86
    21
87
    >>> foo2.foo(11)
88
    running method with 11 0
89
    14
90
    >>> foo2.foo(11, y=7)
91
    21
92
    >>> class Foo (object):
93
    ...   def __init__(self, lower):
94
    ...     self.lower = lower
95
    ...   @memoizemethod
96
    ...   def range_tuple(self, upper):
97
    ...     print('running function')
98
    ...     return tuple(i for i in range(self.lower, upper))
99
    ...   @memoizemethod
100
    ...   def range_iter(self, upper):
101
    ...     print('running function')
102
    ...     return (i for i in range(self.lower, upper))
103
    ...
104
    >>> foo = Foo(3)
105
    >>> foo.range_tuple(6)
106
    running function
107
    (3, 4, 5)
108
    >>> foo.range_tuple(7)
109
    running function
110
    (3, 4, 5, 6)
111
    >>> foo.range_tuple(6)
112
    (3, 4, 5)
113
    >>> foo.range_iter(6)
114
    Traceback (most recent call last):
115
    TypeError: Can't memoize a generator or non-hashable object!
116
    """
117
118
    @wraps(method)
119
    def _wrapper(self, *args, **kwargs):
120
        # NOTE:  a __dict__ check is performed here rather than using the
121
        # built-in hasattr function because hasattr will look up to an object's
122
        # class if the attr is not directly found in the object's dict.  That's
123
        # bad for this if the class itself has a memoized classmethod for
124
        # example that has been called before the memoized instance method,
125
        # then the instance method will use the class's result cache, causing
126
        # its results to be globally stored rather than on a per instance
127
        # basis.
128
        if '_memoized_results' not in self.__dict__:
129
            self._memoized_results = {}
130
        memoized_results = self._memoized_results
131
132
        key = (method.__name__, args, tuple(sorted(kwargs.items())))
133
        if key in memoized_results:
134
            return memoized_results[key]
135
        else:
136
            try:
137
                result = method(self, *args, **kwargs)
138
            except KeyError as e:
139
                if '__wrapped__' in str(e):
140
                    result = None  # is this the right thing to do?  happened during py3 conversion
141
                else:
142
                    raise
143
            if isinstance(result, GeneratorType) or not isinstance(result, Hashable):
144
                raise TypeError("Can't memoize a generator or non-hashable object!")
145
            return memoized_results.setdefault(key, result)
146
147
    return _wrapper
148
149
150
# class memoizemethod(object):
151
#     """cache the return value of a method
152
#
153
#     This class is meant to be used as a decorator of methods. The return value
154
#     from a given method invocation will be cached on the instance whose method
155
#     was invoked. All arguments passed to a method decorated with memoize must
156
#     be hashable.
157
#
158
#     If a memoized method is invoked directly on its class the result will not
159
#     be cached. Instead the method will be invoked like a static method:
160
#     class Obj(object):
161
#         @memoize
162
#         def add_to(self, arg):
163
#             return self + arg
164
#     Obj.add_to(1) # not enough arguments
165
#     Obj.add_to(1, 2) # returns 3, result is not cached
166
#     """
167
#     def __init__(self, func):
168
#         self.func = func
169
#     def __get__(self, obj, objtype=None):
170
#         if obj is None:
171
#             return self.func
172
#         return partial(self, obj)
173
#     def __call__(self, *args, **kw):
174
#         obj = args[0]
175
#         try:
176
#             cache = obj.__cache
177
#         except AttributeError:
178
#             cache = obj.__cache = {}
179
#         key = (self.func, args[1:], frozenset(kw.items()))
180
#         try:
181
#             res = cache[key]
182
#         except KeyError:
183
#             res = cache[key] = self.func(*args, **kw)
184
#         return res
185
186
187
def clear_memoized_methods(obj, *method_names):
188
    """
189
    Clear the memoized method or @memoizeproperty results for the given
190
    method names from the given object.
191
192
    >>> v = [0]
193
    >>> def inc():
194
    ...     v[0] += 1
195
    ...     return v[0]
196
    ...
197
    >>> class Foo(object):
198
    ...    @memoizemethod
199
    ...    def foo(self):
200
    ...        return inc()
201
    ...    @memoizeproperty
202
    ...    def g(self):
203
    ...       return inc()
204
    ...
205
    >>> f = Foo()
206
    >>> f.foo(), f.foo()
207
    (1, 1)
208
    >>> clear_memoized_methods(f, 'foo')
209
    >>> (f.foo(), f.foo(), f.g, f.g)
210
    (2, 2, 3, 3)
211
    >>> (f.foo(), f.foo(), f.g, f.g)
212
    (2, 2, 3, 3)
213
    >>> clear_memoized_methods(f, 'g', 'no_problem_if_undefined')
214
    >>> f.g, f.foo(), f.g
215
    (4, 2, 4)
216
    >>> f.foo()
217
    2
218
    """
219
    for key in list(getattr(obj, '_memoized_results', {}).keys()):
220
        # key[0] is the method name
221
        if key[0] in method_names:
222
            del obj._memoized_results[key]
223
224
    property_dict = obj._cache_
225
    for prop in method_names:
226
        inner_attname = '__%s' % prop
227
        if inner_attname in property_dict:
228
            del property_dict[inner_attname]
229
230
231
def memoizedproperty(func):
232
    """
233
    Decorator to cause a method to cache it's results in self for each
234
    combination of inputs and return the cached result on subsequent calls.
235
    Does not support named arguments or arg values that are not hashable.
236
237
    >>> class Foo (object):
238
    ...   _x = 1
239
    ...   @memoizedproperty
240
    ...   def foo(self):
241
    ...     self._x += 1
242
    ...     print('updating and returning {0}'.format(self._x))
243
    ...     return self._x
244
    ...
245
    >>> foo1 = Foo()
246
    >>> foo2 = Foo()
247
    >>> foo1.foo
248
    updating and returning 2
249
    2
250
    >>> foo1.foo
251
    2
252
    >>> foo2.foo
253
    updating and returning 2
254
    2
255
    >>> foo1.foo
256
    2
257
    """
258
    inner_attname = '__%s' % func.__name__
259
260
    def new_fget(self):
261
        if not hasattr(self, '_cache_'):
262
            self._cache_ = dict()
263
        cache = self._cache_
264
        if inner_attname not in cache:
265
            cache[inner_attname] = func(self)
266
        return cache[inner_attname]
267
268
    return property(new_fget)
269
270
271
# def memoized_property(fget):
272
#     """
273
#     Return a property attribute for new-style classes that only calls its getter on the first
274
#     access. The result is stored and on subsequent accesses is returned, preventing the need to
275
#     call the getter any more.
276
#     Example::
277
#         >>> class C(object):
278
#         ...     load_name_count = 0
279
#         ...     @memoized_property
280
#         ...     def name(self):
281
#         ...         "name's docstring"
282
#         ...         self.load_name_count += 1
283
#         ...         return "the name"
284
#         >>> c = C()
285
#         >>> c.load_name_count
286
#         0
287
#         >>> c.name
288
#         "the name"
289
#         >>> c.load_name_count
290
#         1
291
#         >>> c.name
292
#         "the name"
293
#         >>> c.load_name_count
294
#         1
295
#     """
296
#     attr_name = '_{0}'.format(fget.__name__)
297
#
298
#     @wraps(fget)
299
#     def fget_memoized(self):
300
#         if not hasattr(self, attr_name):
301
#             setattr(self, attr_name, fget(self))
302
#         return getattr(self, attr_name)
303
#
304
#     return property(fget_memoized)
305
306
class classproperty(object):  # pylint: disable=C0103
307
    # from celery.five
308
309
    def __init__(self, getter=None, setter=None):
310
        if getter is not None and not isinstance(getter, classmethod):
311
            getter = classmethod(getter)
312
        if setter is not None and not isinstance(setter, classmethod):
313
            setter = classmethod(setter)
314
        self.__get = getter
315
        self.__set = setter
316
317
        info = getter.__get__(object)  # just need the info attrs.
318
        self.__doc__ = info.__doc__
319
        self.__name__ = info.__name__
320
        self.__module__ = info.__module__
321
322
    def __get__(self, obj, type_=None):
323
        if obj and type_ is None:
324
            type_ = obj.__class__
325
        return self.__get.__get__(obj, type_)()
326
327
    def __set__(self, obj, value):
328
        if obj is None:
329
            return self
330
        return self.__set.__get__(obj)(value)
331
332
    def setter(self, setter):
333
        return self.__class__(self.__get, setter)
334
335
# memoize & clear:
336
#     class method
337
#     function
338
#     classproperty
339
#     property
340
#     staticproperty?
341
# memoizefunction
342
# memoizemethod
343
# memoizeproperty
344
#
345
#
346