505 votes

Qu'est-ce que la mémorisation et comment puis-je l'utiliser en Python ?

Je viens de commencer Python et je n'ai aucune idée de ce que mémorisation est et comment l'utiliser. En outre, puis-je avoir un exemple simplifié ?

286 votes

Lorsque la deuxième phrase de l'article pertinent de wikipedia contient la phrase "mutually-recursive descent parsing[1] in a general top-down parsing algorithm[2][3] that accommodates ambiguity and left recursion in polynomial time and space," je pense qu'il est tout à fait approprié de demander à SO ce qui se passe.

14 votes

@Clueless : Cette phrase est précédée de "La mémorisation a également été utilisée dans d'autres contextes (et à des fins autres que les gains de vitesse), comme dans". Donc, c'est juste une liste d'exemples (et n'a pas besoin d'être compris) ; ça ne fait pas partie de l'explication de la mémoïsation.

0 votes

Voici une bonne explication avec des exemples attachés de la mémorisation et comment l'incorporer dans un décorateur : pycogsci.info/?p=221

433voto

Jason Points 125291

La mémorisation fait effectivement référence au fait de se souvenir ("memoization" → "memorandum" → se souvenir) des résultats des appels de méthode en fonction des entrées de la méthode, puis de renvoyer le résultat mémorisé plutôt que de calculer à nouveau le résultat. Vous pouvez l'imaginer comme un cache pour les résultats des méthodes. Pour plus de détails, reportez-vous à la page 387 pour la définition en Introduction aux algorithmes (3e), Cormen et al.

Un exemple simple pour calculer des factorielles en utilisant la mémorisation en Python serait quelque chose comme ceci :

factorial_memo = {}
def factorial(k):
    if k < 2: return 1
    if k not in factorial_memo:
        factorial_memo[k] = k * factorial(k-1)
    return factorial_memo[k]

Vous pouvez être plus compliqué et encapsuler le processus de mémorisation dans une classe :

class Memoize:
    def __init__(self, f):
        self.f = f
        self.memo = {}
    def __call__(self, *args):
        if not args in self.memo:
            self.memo[args] = self.f(*args)
        #Warning: You may wish to do a deepcopy here if returning objects
        return self.memo[args]

Entonces:

def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

factorial = Memoize(factorial)

Une fonction connue sous le nom de " décorateurs " a été ajouté dans Python 2.4, ce qui vous permet maintenant d'écrire simplement ce qui suit pour accomplir la même chose :

@Memoize
def factorial(k):
    if k < 2: return 1
    return k * factorial(k - 1)

En Bibliothèque de décorateurs Python possède un décorateur similaire appelé memoized qui est légèrement plus robuste que le Memoize montré ici.

0 votes

Je teste vos deux exemples dans le %timeit d'IPython. Lorsque j'utilise le dictionnaire (le premier exemple), la plupart du temps, les premiers appels sont exécutés plus rapidement que les seconds appels mémorisés. Pourriez-vous également vérifier dans votre système ? Il y a une grande accélération (jusqu'à 40X) quand j'ai testé le deuxième exemple. Pourriez-vous également me dire comment accéder au nom self.memo plus tard, lorsque l'exécution est terminée ?

0 votes

OK, veuillez ignorer la première partie de ma question car le timeit d'IPython fait plusieurs appels tout en chronométrant l'exécution d'une fonction. Cependant, la partie self.memo est toujours valable :)

3 votes

Merci pour cette suggestion. La classe Memoize est une solution élégante qui peut facilement être appliquée au code existant sans nécessiter de refactoring important.

383voto

Flimm Points 8870

functools.cache décorateur :

Python 3.9 a publié une nouvelle fonction functools.cache . Il met en cache en mémoire le résultat d'une fonction appelée avec un ensemble particulier d'arguments, ce qui est de la mémoïsation. C'est facile à utiliser :

import functools

@functools.cache
def fib(num):
    if num < 2:
        return num
    else:
        return fib(num-1) + fib(num-2)

Cette fonction sans le décorateur est très lente, essayez donc fib(36) et vous devrez attendre une dizaine de secondes.

Ajout de la cache garantit que si la fonction a été appelée récemment pour une valeur particulière, elle ne recalculera pas cette valeur, mais utilisera un résultat précédent mis en cache. Dans ce cas, cela conduit à une amélioration considérable de la vitesse, alors que le code n'est pas encombré par les détails de la mise en cache.

functools.lru_cache décorateur :

Si vous devez prendre en charge d'anciennes versions de Python, functools.lru_cache fonctionne dans Python 3.2+. Par défaut, il ne met en cache que les 128 appels les plus récemment utilisés, mais vous pouvez définir l'attribut maxsize a None pour indiquer que le cache ne doit jamais expirer :

@functools.lru_cache(maxsize=None)
def fib(num):
    # etc

2 votes

J'ai essayé fib(1000) et j'ai obtenu RecursionError : la profondeur maximale de récursion a été dépassée dans la comparaison.

7 votes

@Andyk La limite de récursion par défaut de Py3 est de 1000. La première fois que fib est appelé, il devra revenir au cas de base avant que la mémorisation puisse avoir lieu. Donc, votre comportement est tout à fait attendu.

2 votes

Si je ne me trompe pas, il ne met en cache que tant que le processus n'est pas tué, n'est-ce pas ? Ou est-ce qu'il met en cache indépendamment du fait que le processus soit tué ? Par exemple, si je redémarre mon système, les résultats mis en cache le seront-ils toujours ?

64voto

Noufal Ibrahim Points 32200

Les autres réponses couvrent assez bien ce que c'est. Je ne vais pas répéter cela. Juste quelques points qui pourraient vous être utiles.

En général, la mémorisation est une opération que vous pouvez appliquer à toute fonction qui calcule quelque chose (coûteux) et renvoie une valeur. Pour cette raison, elle est souvent implémentée en tant que décorateur . La mise en œuvre est simple et ressemblerait à ceci

memoised_function = memoise(actual_function)

ou exprimé comme un décorateur

@memoise
def actual_function(arg1, arg2):
   #body

20voto

Bryan Oakley Points 63365

La mémorisation consiste à conserver les résultats de calculs coûteux et à renvoyer le résultat mis en cache plutôt que de le recalculer en permanence.

Voici un exemple :

def doSomeExpensiveCalculation(self, input):
    if input not in self.cache:
        <do expensive calculation>
        self.cache[input] = result
    return self.cache[input]

Une description plus complète peut être trouvée dans le entrée wikipedia sur la mémorisation .

0 votes

Hmm, maintenant si c'était du Python correct, ça marcherait, mais il semble que ce ne soit pas le cas... ok, donc "cache" n'est pas un dict ? Parce que si c'est le cas, ça devrait être if input not in self.cache y self.cache[input] ( has_key est obsolète depuis... le début de la série 2.x, si ce n'est 2.0. self.cache(index) n'a jamais été correcte. IIRC)

4voto

singular Points 508

Voici une solution qui fonctionnera avec des arguments de type liste ou dict sans pleurnicher :

def memoize(fn):
    """returns a memoized version of any function that can be called
    with the same list of arguments.
    Usage: foo = memoize(foo)"""

    def handle_item(x):
        if isinstance(x, dict):
            return make_tuple(sorted(x.items()))
        elif hasattr(x, '__iter__'):
            return make_tuple(x)
        else:
            return x

    def make_tuple(L):
        return tuple(handle_item(x) for x in L)

    def foo(*args, **kwargs):
        items_cache = make_tuple(sorted(kwargs.items()))
        args_cache = make_tuple(args)
        if (args_cache, items_cache) not in foo.past_calls:
            foo.past_calls[(args_cache, items_cache)] = fn(*args,**kwargs)
        return foo.past_calls[(args_cache, items_cache)]
    foo.past_calls = {}
    foo.__name__ = 'memoized_' + fn.__name__
    return foo

Notez que cette approche peut être naturellement étendue à n'importe quel objet en implémentant votre propre fonction de hachage comme un cas spécial dans handle_item. Par exemple, pour que cette approche fonctionne pour une fonction qui prend un ensemble comme argument d'entrée, vous pouvez ajouter à handle_item :

if is_instance(x, set):
    return make_tuple(sorted(list(x)))

1 votes

Belle tentative. Sans pleurnicher, un list argument de [1, 2, 3] peut être considérée à tort comme identique à une autre set avec une valeur de {1, 2, 3} . De plus, les ensembles ne sont pas ordonnés comme les dictionnaires, ils doivent donc également être sorted() . Notez également qu'un argument récursif de structure de données provoquerait une boucle infinie.

0 votes

Oui, les ensembles doivent être traités par un boîtier spécial handle_item(x) et un tri. Je n'aurais pas dû dire que cette implémentation gère les ensembles, parce qu'elle ne le fait pas - mais le fait est qu'elle peut être facilement étendue pour le faire en casant spécialement handle_item, et la même chose fonctionnera pour n'importe quelle classe ou objet itérable tant que vous êtes prêt à écrire la fonction de hachage vous-même. La partie la plus délicate - traiter des listes multidimensionnelles ou des dictionnaires - est déjà traitée ici, donc j'ai trouvé que cette fonction memoize est beaucoup plus facile à utiliser comme base que les types simples "je ne prends que des arguments hachables".

0 votes

Le problème que j'ai mentionné est dû au fait que list et set sont "tuplés" dans la même chose et deviennent donc indiscernables les uns des autres. L'exemple de code pour l'ajout de la prise en charge de sets décrite dans votre dernière mise à jour n'évite pas cela, j'en ai peur. On peut facilement s'en rendre compte en passant séparément [1,2,3] y {1,2,3} comme argument à une fonction de test "mémorisée" et voir si elle est appelée deux fois, comme il se doit, ou non.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X