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

378

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

blur959
la source
215
Lorsque la deuxième phrase de l'article wikipedia pertinent contient la phrase "analyse de descente mutuellement récursive [1] dans un algorithme d'analyse descendante général [2] [3] qui tient compte de l'ambiguïté et de la récursion gauche dans le temps et l'espace polynomiaux", je pense il est tout à fait approprié de demander à SO ce qui se passe.
Clueless
10
@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". Ce n'est donc qu'une liste d'exemples (et n'a pas besoin d'être compris); cela ne fait pas partie de l'explication de la mémorisation.
ShreevatsaR
1
@StefanGruenwald Ce lien est mort. Pouvez-vous trouver une mise à jour?
JS.
2
Nouveau lien vers le fichier pdf, puisque pycogsci.info est en panne: people.ucsc.edu/~abrsvn/NLTK_parsing_demos.pdf
Stefan Gruenwald
4
@Clueless, l'article dit en fait " simple analyse de descente mutuellement récursive [1] dans un algorithme général d'analyse descendante [2] [3] qui tient compte de l'ambiguïté et de la récursion gauche dans le temps et l'espace polynomiaux". Vous avez manqué le simple , ce qui rend évidemment cet exemple beaucoup plus clair :).
studgeek

Réponses:

353

La mémorisation se réfère effectivement à la mémorisation ("mémorisation" → "mémorandum" → à mémoriser) des résultats d'appels de méthode en fonction des entrées de méthode, puis au retour du résultat mémorisé plutôt que de calculer à nouveau le résultat. Vous pouvez le considérer comme un cache pour les résultats de la méthode. Pour plus de détails, voir page 387 pour la définition dans Introduction To Algorithms (3e), Cormen et al.

Un exemple simple de calcul de factorielles 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 devenir 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]

Alors:

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

factorial = Memoize(factorial)

Une fonctionnalité connue sous le nom de " décorateurs " a été ajoutée dans Python 2.4 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)

La bibliothèque Python Decorator possède un décorateur similaire appelé memoizedqui est légèrement plus robuste que la Memoizeclasse présentée ici.

Jason
la source
2
Merci pour cette suggestion. La classe Memoize est une solution élégante qui peut facilement être appliquée au code existant sans avoir besoin de beaucoup de refactoring.
Captain Lepton
10
La solution de la classe Memoize est boguée, elle ne fonctionnera pas de la même manière que la factorial_memo, car l' factorialintérieur def factorialappelle toujours l'ancien unmemize factorial.
adamsmith
9
Soit dit en passant, vous pouvez également écrire if k not in factorial_memo:, qui lit mieux que if not k in factorial_memo:.
ShreevatsaR
5
Devrait vraiment faire cela en tant que décorateur.
Emlyn O'Regan
3
@ durden2.0 Je sais que c'est un vieux commentaire, mais argsc'est un tuple. def some_function(*args)fait d'args un tuple.
Adam Smith
232

Nouveau dans Python 3.2 est functools.lru_cache. Par défaut, il met en cache uniquement les 128 plus récemment des appels utilisés, mais vous pouvez définir l' maxsizeà Noneindiquer que le cache ne doit jamais expirer:

import functools

@functools.lru_cache(maxsize=None)
def fib(num):
    if num < 2:
        return num
    else:
        return fib(num-1) + fib(num-2)

Cette fonction en elle-même est très lente, essayez fib(36)et vous devrez attendre une dizaine de secondes.

L'ajout d' lru_cacheannotations 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, tandis que le code n'est pas encombré par les détails de la mise en cache.

Flimm
la source
2
Fib essayé (1000), obtenu RecursionError: la profondeur maximale de récursivité a été dépassée en comparaison
X Æ A-12
5
@Andyk La limite de récursivité Py3 par défaut est de 1000. La première fois que vous l'appelez fib, elle devra se reproduire dans le cas de base avant que la mémorisation ne puisse avoir lieu. Donc, votre comportement est à peu près attendu.
Quelklef
1
Si je ne me trompe pas, il ne se cache que jusqu'à ce que le processus ne soit pas tué, non? Ou cache-t-il que le processus soit interrompu ou non? Par exemple, disons que je redémarre mon système - les résultats mis en cache seront-ils toujours mis en cache?
Kristada673
1
@ Kristada673 Oui, il est stocké dans la mémoire du processus, pas sur le disque.
Flimm
2
Notez que cela accélère même la première exécution de la fonction, car il s'agit d'une fonction récursive et met en cache ses propres résultats intermédiaires. Cela pourrait être bon pour illustrer une fonction non récursive qui est juste intrinsèquement lente pour la rendre plus claire pour les nuls comme moi. : D
endolith
61

Les autres réponses couvrent assez bien ce que c'est. Je ne répète pas ça. Juste quelques points qui pourraient vous être utiles.

Habituellement, la mémoisation est une opération que vous pouvez appliquer à n'importe quelle fonction qui calcule quelque chose (cher) et renvoie une valeur. Pour cette raison, il est souvent mis en œuvre en tant que décorateur . L'implémentation est simple et ce serait quelque chose comme ça

memoised_function = memoise(actual_function)

ou exprimé en tant que décorateur

@memoise
def actual_function(arg1, arg2):
   #body
Noufal Ibrahim
la source
18

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 continu.

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 l' entrée wikipedia sur la mémorisation .

Bryan Oakley
la source
Hmm, maintenant si c'était correct en Python, ça basculerait, mais ça ne semble pas être ... d'accord, donc "cache" n'est pas un dicton? Parce que si c'est le cas, cela devrait être if input not in self.cache et self.cache[input] ( has_keyest obsolète depuis ... au début de la série 2.x, sinon 2.0 self.cache(index). N'a jamais été correct. IIRC)
Jürgen A. Erhard
15

N'oublions pas la hasattrfonction intégrée, pour ceux qui veulent fabriquer à la main. De cette façon, vous pouvez conserver le cache mem dans la définition de la fonction (par opposition à un global).

def fact(n):
    if not hasattr(fact, 'mem'):
        fact.mem = {1: 1}
    if not n in fact.mem:
        fact.mem[n] = n * fact(n - 1)
    return fact.mem[n]
Karel Kubat
la source
Cela semble être une idée très coûteuse. Pour chaque n, il met non seulement en cache les résultats pour n, mais aussi pour 2 ... n-1.
codeforester
15

J'ai trouvé cela extrêmement utile

def memoize(function):
    from functools import wraps

    memo = {}

    @wraps(function)
    def wrapper(*args):
        if args in memo:
            return memo[args]
        else:
            rv = function(*args)
            memo[args] = rv
            return rv
    return wrapper


@memoize
def fibonacci(n):
    if n < 2: return n
    return fibonacci(n - 1) + fibonacci(n - 2)

fibonacci(25)
mr.bjerre
la source
Voir docs.python.org/3/library/functools.html#functools.wraps pour savoir pourquoi utiliser functools.wraps.
anishpatel
1
Dois-je effacer manuellement le memoafin que la mémoire soit libérée?
nos
L'idée est que les résultats sont stockés dans un mémo au sein d'une session. C'est-à-dire que rien n'est effacé tel quel
m
6

La mémorisation consiste essentiellement à sauvegarder les résultats des opérations passées effectuées avec des algorithmes récursifs afin de réduire la nécessité de parcourir l'arbre de récursivité si le même calcul est requis à un stade ultérieur.

voir http://scriptbucket.wordpress.com/2012/12/11/introduction-to-memoization/

Exemple de mémorisation de Fibonacci en Python:

fibcache = {}
def fib(num):
    if num in fibcache:
        return fibcache[num]
    else:
        fibcache[num] = num if num < 2 else fib(num-1) + fib(num-2)
        return fibcache[num]
Romaine Carter
la source
2
Pour plus de performances, pré-amorcez votre fibcache avec les premières valeurs connues, vous pouvez alors extraire la logique supplémentaire pour les gérer du «hot path» du code.
jkflying
5

La mémorisation est la conversion de fonctions en structures de données. Habituellement, on veut que la conversion se fasse de manière incrémentielle et paresseuse (à la demande d'un élément de domaine donné - ou "clé"). Dans les langages fonctionnels paresseux, cette conversion paresseuse peut se produire automatiquement, et ainsi la mémorisation peut être implémentée sans effets secondaires (explicites).

Conal
la source
5

Eh bien, je devrais d'abord répondre à la première partie: qu'est-ce que la mémorisation?

C'est juste une méthode pour échanger de la mémoire contre du temps. Pensez à la table de multiplication .

L'utilisation d'un objet mutable comme valeur par défaut dans Python est généralement considérée comme mauvaise. Mais si vous l'utilisez à bon escient, il peut être utile d'implémenter a memoization.

Voici un exemple adapté de http://docs.python.org/2/faq/design.html#why-are-default-values-shared-between-objects

En utilisant un mutable dictdans la définition de la fonction, les résultats intermédiaires calculés peuvent être mis en cache (par exemple lors du calcul factorial(10)après calcul factorial(9), nous pouvons réutiliser tous les résultats intermédiaires)

def factorial(n, _cache={1:1}):    
    try:            
        return _cache[n]           
    except IndexError:
        _cache[n] = factorial(n-1)*n
        return _cache[n]
yegle
la source
4

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 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)))
RussellStewart
la source
1
Belle tentative. Sans pleurnicher, un listargument de [1, 2, 3]peut à tort être considéré comme un setargument différent avec une valeur de {1, 2, 3}. De plus, les ensembles ne sont pas ordonnés comme les dictionnaires, ils devraient donc également l'être sorted(). Notez également qu'un argument de structure de données récursive provoquerait une boucle infinie.
martineau
Oui, les ensembles doivent être géré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, car elle ne le fait pas - mais le fait est qu'elle peut être facilement étendue pour le faire par un boîtier spécial handle_item, et la même chose fonctionnera pour n'importe quelle classe ou objet itérable tant que vous êtes prêt à écrire vous-même la fonction de hachage. La partie délicate - traiter des listes ou dictionnaires multidimensionnels - est déjà traitée ici, j'ai donc constaté que cette fonction de mémorisation est beaucoup plus facile à utiliser comme base que les simples types "Je ne prends que des arguments lavables".
RussellStewart
Le problème que j'ai mentionné est dû au fait que lists et sets sont «tupleisés» dans la même chose et deviennent donc indiscernables l'un de l'autre. L'exemple de code pour ajouter le support pour setsdécrit dans votre dernière mise à jour n'évite pas que j'en ai peur. Cela peut facilement être vu en passant séparément [1,2,3]et {1,2,3}en tant qu'argument à une fonction de test d "mémoize" et en voyant si elle est appelée deux fois, comme il se doit, ou non.
martineau
oui, j'ai lu ce problème, mais je ne l'ai pas abordé parce que je pense qu'il est beaucoup plus mineur que l'autre que vous avez mentionné. À quand remonte la dernière fois que vous avez écrit une fonction mémorisée où un argument fixe pourrait être soit une liste ou un ensemble, et les deux ont abouti à des sorties différentes? Si vous deviez rencontrer un cas aussi rare, il vous suffirait de réécrire handle_item pour ajouter un préfixe, disons 0 si l'élément est un ensemble, ou 1 s'il s'agit d'une liste.
RussellStewart
En fait, il y a un problème similaire avec lists et dicts car il est possible que a listcontienne exactement la même chose qui résulte de l'appel make_tuple(sorted(x.items()))à un dictionnaire. Une solution simple pour les deux cas serait d'inclure la type()valeur dans le tuple généré. Je peux penser à un moyen encore plus simple de gérer spécifiquement les sets, mais cela ne se généralise pas.
martineau
3

Solution qui fonctionne avec les arguments de position et de mot-clé indépendamment de l'ordre dans lequel les arguments de mot-clé ont été passés (en utilisant inspect.getargspec ):

import inspect
import functools

def memoize(fn):
    cache = fn.cache = {}
    @functools.wraps(fn)
    def memoizer(*args, **kwargs):
        kwargs.update(dict(zip(inspect.getargspec(fn).args, args)))
        key = tuple(kwargs.get(k, None) for k in inspect.getargspec(fn).args)
        if key not in cache:
            cache[key] = fn(**kwargs)
        return cache[key]
    return memoizer

Question similaire: L' identification de la fonction varargs équivalente appelle la mémorisation en Python

ndpu
la source
2
cache = {}
def fib(n):
    if n <= 1:
        return n
    else:
        if n not in cache:
            cache[n] = fib(n-1) + fib(n-2)
        return cache[n]
Vikrant Singh
la source
4
vous pouvez utiliser simplement à la if n not in cacheplace. l'utilisation cache.keyscréerait une liste inutile en python 2
n611x007
2

Je voulais juste ajouter aux réponses déjà fournies, la bibliothèque de décorateur Python a quelques implémentations simples mais utiles qui peuvent également mémoriser des "types non partageables", contrairement à functools.lru_cache.

Sid
la source
1
Ce décorateur ne mémorise pas les "types inébranlables" ! Cela revient simplement à appeler la fonction sans mémorisation, aller à l'encontre de l' explicite vaut mieux que du dogme implicite .
ostrokach