# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals
import functools
import sys
from ubelt import util_hash
def _hashable(item):
"""
Returns the item if it is naturally hashable, otherwise it tries to use
ub.hash_data to make it hashable. Errors if it cannot.
"""
try:
hash(item)
except TypeError:
return util_hash.hash_data(item)
else:
return item
def _make_signature_key(args, kwargs):
"""
Transforms function args into a key that can be used by the cache
CommandLine:
python -m ubelt.util_decor _make_signature_key
Example:
>>> args = (4, [1, 2])
>>> kwargs = {'a': 'b'}
>>> key = _make_signature_key(args, kwargs)
>>> print('key = {!r}'.format(key))
>>> # Some mutable types cannot be handled by ub.hash_data
>>> import pytest
>>> import six
>>> if six.PY2:
>>> import collections as abc
>>> else:
>>> from collections import abc
>>> with pytest.raises(TypeError):
>>> _make_signature_key((4, [1, 2], {1: 2, 'a': 'b'}), kwargs={})
>>> class Dummy(abc.MutableSet):
>>> def __contains__(self, item): return None
>>> def __iter__(self): return iter([])
>>> def __len__(self): return 0
>>> def add(self, item, loc): return None
>>> def discard(self, item): return None
>>> with pytest.raises(TypeError):
>>> _make_signature_key((Dummy(),), kwargs={})
"""
kwitems = kwargs.items()
# TODO: we should check if Python is at least 3.7 and sort by kwargs
# keys otherwise. Should we use hash_data for key generation
if (sys.version_info.major, sys.version_info.minor) < (3, 7): # nocover
# We can sort because they keys are gaurenteed to be strings
kwitems = sorted(kwitems)
kwitems = tuple(kwitems)
try:
key = _hashable(args), _hashable(kwitems)
except TypeError:
raise TypeError('Signature is not hashable: args={} kwargs{}'.format(args, kwargs))
return key
[docs]def memoize(func):
"""
memoization decorator that respects args and kwargs
References:
https://wiki.python.org/moin/PythonDecoratorLibrary#Memoize
Args:
func (function): live python function
Returns:
func: memoized wrapper
CommandLine:
python -m ubelt.util_decor memoize
Example:
>>> import ubelt as ub
>>> closure = {'a': 'b', 'c': 'd'}
>>> incr = [0]
>>> def foo(key):
>>> value = closure[key]
>>> incr[0] += 1
>>> return value
>>> foo_memo = ub.memoize(foo)
>>> assert foo('a') == 'b' and foo('c') == 'd'
>>> assert incr[0] == 2
>>> print('Call memoized version')
>>> assert foo_memo('a') == 'b' and foo_memo('c') == 'd'
>>> assert incr[0] == 4
>>> assert foo_memo('a') == 'b' and foo_memo('c') == 'd'
>>> print('Counter should no longer increase')
>>> assert incr[0] == 4
>>> print('Closure changes result without memoization')
>>> closure = {'a': 0, 'c': 1}
>>> assert foo('a') == 0 and foo('c') == 1
>>> assert incr[0] == 6
>>> assert foo_memo('a') == 'b' and foo_memo('c') == 'd'
"""
cache = {}
@functools.wraps(func)
def memoizer(*args, **kwargs):
key = _make_signature_key(args, kwargs)
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
memoizer.cache = cache
return memoizer
[docs]class memoize_method(object):
"""
memoization decorator for a method that respects args and kwargs
References:
http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/
Example:
>>> import ubelt as ub
>>> closure = {'a': 'b', 'c': 'd'}
>>> incr = [0]
>>> class Foo(object):
>>> @memoize_method
>>> def foo_memo(self, key):
>>> value = closure[key]
>>> incr[0] += 1
>>> return value
>>> def foo(self, key):
>>> value = closure[key]
>>> incr[0] += 1
>>> return value
>>> self = Foo()
>>> assert self.foo('a') == 'b' and self.foo('c') == 'd'
>>> assert incr[0] == 2
>>> print('Call memoized version')
>>> assert self.foo_memo('a') == 'b' and self.foo_memo('c') == 'd'
>>> assert incr[0] == 4
>>> assert self.foo_memo('a') == 'b' and self.foo_memo('c') == 'd'
>>> print('Counter should no longer increase')
>>> assert incr[0] == 4
>>> print('Closure changes result without memoization')
>>> closure = {'a': 0, 'c': 1}
>>> assert self.foo('a') == 0 and self.foo('c') == 1
>>> assert incr[0] == 6
>>> assert self.foo_memo('a') == 'b' and self.foo_memo('c') == 'd'
>>> print('Constructing a new object should get a new cache')
>>> self2 = Foo()
>>> self2.foo_memo('a')
>>> assert incr[0] == 7
>>> self2.foo_memo('a')
>>> assert incr[0] == 7
"""
def __init__(self, func):
self._func = func
self._cache_name = '_cache__' + func.__name__
def __get__(self, instance, cls=None):
"""
Descriptor get method. Called when the decorated method is accessed
from an object instance.
Args:
instance (object): the instance of the class with the memoized method
cls (type): the type of the instance
"""
self._instance = instance
return self
def __call__(self, *args, **kwargs):
"""
The wrapped function call
"""
cache = self._instance.__dict__.setdefault(self._cache_name, {})
key = _make_signature_key(args, kwargs)
if key in cache:
return cache[key]
else:
value = cache[key] = self._func(self._instance, *args, **kwargs)
return value
if __name__ == '__main__':
r"""
CommandLine:
python -m ubelt.util_decor all
"""
import xdoctest
xdoctest.doctest_module(__file__)