memoizemethod()   D
last analyzed

Complexity

Conditions 8

Size

Total Lines 83

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
c 1
b 0
f 0
dl 0
loc 83
rs 4.5767

1 Method

Rating   Name   Duplication   Size   Complexity  
C _wrapper() 0 28 7

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    try:
225
        property_dict = obj._cache_
226
    except AttributeError:
227
        property_dict = obj._cache_ = {}
228
229
    for prop in method_names:
230
        inner_attname = '__%s' % prop
231
        if inner_attname in property_dict:
232
            del property_dict[inner_attname]
233
234
235
def memoizeproperty(func):
236
    """
237
    Decorator to cause a method to cache it's results in self for each
238
    combination of inputs and return the cached result on subsequent calls.
239
    Does not support named arguments or arg values that are not hashable.
240
241
    >>> class Foo (object):
242
    ...   _x = 1
243
    ...   @memoizeproperty
244
    ...   def foo(self):
245
    ...     self._x += 1
246
    ...     print('updating and returning {0}'.format(self._x))
247
    ...     return self._x
248
    ...
249
    >>> foo1 = Foo()
250
    >>> foo2 = Foo()
251
    >>> foo1.foo
252
    updating and returning 2
253
    2
254
    >>> foo1.foo
255
    2
256
    >>> foo2.foo
257
    updating and returning 2
258
    2
259
    >>> foo1.foo
260
    2
261
    """
262
    inner_attname = '__%s' % func.__name__
263
264
    def new_fget(self):
265
        try:
266
            cache = self._cache_
267
        except AttributeError:
268
            cache = self._cache_ = {}
269
        if inner_attname not in cache:
270
            cache[inner_attname] = func(self)
271
        return cache[inner_attname]
272
273
    return property(new_fget)
274
275
276
memoizedproperty = memoizeproperty
277
278
# def memoized_property(fget):
279
#     """
280
#     Return a property attribute for new-style classes that only calls its getter on the first
281
#     access. The result is stored and on subsequent accesses is returned, preventing the need to
282
#     call the getter any more.
283
#     Example::
284
#         >>> class C(object):
285
#         ...     load_name_count = 0
286
#         ...     @memoized_property
287
#         ...     def name(self):
288
#         ...         "name's docstring"
289
#         ...         self.load_name_count += 1
290
#         ...         return "the name"
291
#         >>> c = C()
292
#         >>> c.load_name_count
293
#         0
294
#         >>> c.name
295
#         "the name"
296
#         >>> c.load_name_count
297
#         1
298
#         >>> c.name
299
#         "the name"
300
#         >>> c.load_name_count
301
#         1
302
#     """
303
#     attr_name = '_{0}'.format(fget.__name__)
304
#
305
#     @wraps(fget)
306
#     def fget_memoized(self):
307
#         if not hasattr(self, attr_name):
308
#             setattr(self, attr_name, fget(self))
309
#         return getattr(self, attr_name)
310
#
311
#     return property(fget_memoized)
312
313
class classproperty(object):  # pylint: disable=C0103
314
    # from celery.five
315
316
    def __init__(self, getter=None, setter=None):
317
        if getter is not None and not isinstance(getter, classmethod):
318
            getter = classmethod(getter)
319
        if setter is not None and not isinstance(setter, classmethod):
320
            setter = classmethod(setter)
321
        self.__get = getter
322
        self.__set = setter
323
324
        info = getter.__get__(object)  # just need the info attrs.
325
        self.__doc__ = info.__doc__
326
        self.__name__ = info.__name__
327
        self.__module__ = info.__module__
328
329
    def __get__(self, obj, type_=None):
330
        if obj and type_ is None:
331
            type_ = obj.__class__
332
        return self.__get.__get__(obj, type_)()
333
334
    def __set__(self, obj, value):
335
        if obj is None:
336
            return self
337
        return self.__set.__get__(obj)(value)
338
339
    def setter(self, setter):
340
        return self.__class__(self.__get, setter)
341
342
# memoize & clear:
343
#     class method
344
#     function
345
#     classproperty
346
#     property
347
#     staticproperty?
348
# memoizefunction
349
# memoizemethod
350
# memoizeproperty
351
#
352
#
353