Completed
Push — develop ( 3606a5...c5328e )
by Kale
01:06
created

memoizemethod()   D

Complexity

Conditions 8

Size

Total Lines 83

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 8
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
    property_dict = obj.__dict__
225
    for prop in method_names:
226
        inner_attname = '_{0}'.format(prop)
227
        if inner_attname in property_dict:
228
            del property_dict[inner_attname]
229
230
231
def memoizeproperty(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
    ...   @memoizeproperty
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 = '_{0}'.format(func.__name__)
259
260
    def new_fget(self):
261
        self_dict = self.__dict__
262
        if inner_attname not in self_dict:
263
            self_dict[inner_attname] = func(self)
264
        return self_dict[inner_attname]
265
266
    return property(new_fget)
267
268
269
class classproperty(object):  # pylint: disable=C0103
270
    # from celery.five
271
272
    def __init__(self, getter=None, setter=None):
273
        if getter is not None and not isinstance(getter, classmethod):
274
            getter = classmethod(getter)
275
        if setter is not None and not isinstance(setter, classmethod):
276
            setter = classmethod(setter)
277
        self.__get = getter
278
        self.__set = setter
279
280
        info = getter.__get__(object)  # just need the info attrs.
281
        self.__doc__ = info.__doc__
282
        self.__name__ = info.__name__
283
        self.__module__ = info.__module__
284
285
    def __get__(self, obj, type_=None):
286
        if obj and type_ is None:
287
            type_ = obj.__class__
288
        return self.__get.__get__(obj, type_)()
289
290
    def __set__(self, obj, value):
291
        if obj is None:
292
            return self
293
        return self.__set.__get__(obj)(value)
294
295
    def setter(self, setter):
296
        return self.__class__(self.__get, setter)
297