J'aimerais activer ou désactiver un "cache" dans une méthode de classe pendant l'exécution.

J'ai trouvé un moyen de l'activer avec quelque chose comme ça:

(...)
setattr(self, "_greedy_function", my_cache_decorator(self._cache)(getattr(self, "_greedy_function")))
(...)

self._cache est mon propre objet de cache qui stocke les résultats de self._greedy_function.

Cela fonctionne bien, mais maintenant, que faire si je veux désactiver le cache et "décorer" _greedy_function ?

Je vois une solution possible, stocker la référence de _greedy_function avant de la décorer mais il existe peut-être un moyen de la récupérer à partir de la fonction décorée et ce serait mieux.

Comme demandé, voici le décorateur et l'objet de cache que j'utilise pour mettre en cache les résultats de mes fonctions de classe :

import logging
from collections import OrderedDict, namedtuple
from functools import wraps

logging.basicConfig(
    level=logging.WARNING,
    format='%(asctime)s %(name)s %(levelname)s %(message)s'
)

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

CacheInfo = namedtuple("CacheInfo", "hits misses maxsize currsize")

def lru_cache(cache):
    """
    A replacement for functools.lru_cache() build on a custom LRU Class.
    It can cache class methods.
    """
    def decorator(func):
        logger.debug("assigning cache %r to function %s" % (cache, func.__name__))
        @wraps(func)
        def wrapped_func(*args, **kwargs):
            try:
                ret = cache[args]
                logger.debug("cached value returned for function %s" % func.__name__)
                return ret
            except KeyError:
                try:
                    ret = func(*args, **kwargs)
                except:
                    raise
                else:
                    logger.debug("cache updated for function %s" % func.__name__)
                    cache[args] = ret
                    return ret
        return wrapped_func
    return decorator

class LRU(OrderedDict):
    """
    Custom implementation of a LRU cache, build on top of an Ordered dict.
    """
    __slots__ = "_hits", "_misses", "_maxsize"

    def __new__(cls, maxsize=128):
        if maxsize is None:
            return None
        return super().__new__(cls, maxsize=maxsize)

    def __init__(self, maxsize=128, *args, **kwargs):
        self.maxsize = maxsize
        self._hits = 0
        self._misses = 0
        super().__init__(*args, **kwargs)

    def __getitem__(self, key):
        try:
            value = super().__getitem__(key)
        except KeyError:
            self._misses += 1
            raise
        else:
            self.move_to_end(key)
            self._hits += 1
            return value

    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        if len(self) > self._maxsize:
            oldest, = next(iter(self))
            del self[oldest]

    def __delitem__(self, key):
        try:
            super().__delitem__((key,))
        except KeyError:
            pass

    def __repr__(self):
        return "<%s object at %s: %s>" % (self.__class__.__name__, hex(id(self)), self.cache_info())

    def cache_info(self):
        return CacheInfo(self._hits, self._misses, self._maxsize, len(self))

    def clear(self):
        super().clear()
        self._hits, self._misses = 0, 0

    @property
    def maxsize(self):
        return self._maxsize

    @maxsize.setter
    def maxsize(self, maxsize):
        if not isinstance(maxsize, int):
            raise TypeError
        elif maxsize < 2:
            raise ValueError
        elif maxsize & (maxsize - 1) != 0:
            logger.warning("LRU feature performs best when maxsize is a power-of-two, maybe.")
        while maxsize < len(self):
            oldest, = next(iter(self))
            print(oldest)
            del self[oldest]
        self._maxsize = maxsize

Modifier : j'ai mis à jour mon code en utilisant l'attribut __wrapped__ suggéré dans les commentaires et cela fonctionne bien ! Le tout est ici : https://gist.github.com/fbparis/b3ddd5673b603b42c880974dab23db7c ( méthode kik.set_cache()...)

4
fbparis 18 mars 2019 à 03:10

2 réponses

Meilleure réponse

Les versions modernes de functools.wraps installent la fonction d'origine en tant qu'attribut __wrapped__ sur les wrappers qu'elles créent. (On pourrait rechercher dans __closure__ les fonctions imbriquées généralement utilisées à cette fin, mais d'autres types pourraient également être utilisés.) Il est raisonnable de s'attendre à ce que n'importe quel wrapper suive cette convention.

Une alternative est d'avoir un wrapper permanent qui peut être contrôlé par un drapeau, afin qu'il puisse être activé et désactivé sans le supprimer ni le rétablir. Cela a l'avantage que le wrapper peut conserver son état (ici, les valeurs mises en cache). Le drapeau peut être une variable distincte (par exemple, un autre attribut sur un objet portant la fonction enveloppée, le cas échéant) ou peut être un attribut sur le wrapper lui-même.

2
Davis Herring 19 mars 2019 à 06:50

Vous avez rendu les choses trop compliquées. Le décorateur peut être simplement retiré par del self._greedy_function. Il n'est pas nécessaire d'avoir un attribut __wrapped__.

Voici une implémentation minimale des méthodes set_cache et unset_cache :

class LRU(OrderedDict):
    def __init__(self, maxsize=128, *args, **kwargs):
        # ...
        self._cache = dict()
        super().__init__(*args, **kwargs)

    def _greedy_function(self):
        time.sleep(1)
        return time.time()

    def set_cache(self):
        self._greedy_function = lru_cache(self._cache)(getattr(self, "_greedy_function"))

    def unset_cache(self):
        del self._greedy_function

À l'aide de votre décorateur lru_cache, voici les résultats

o = LRU()
o.set_cache()
print('First call', o._greedy_function())
print('Second call',o._greedy_function()) # Here it prints out the cached value
o.unset_cache()
print('Third call', o._greedy_function()) # The cache is not used

Les sorties

First call 1552966668.735025
Second call 1552966668.735025
Third call 1552966669.7354007
4
gdlmx 19 mars 2019 à 03:41