Cos'è la memoization e come posso usarla in Python?


378

Ho appena avviato Python e non ho idea di cosa sia la memoizzazione e come utilizzarla. Inoltre, posso avere un esempio semplificato?


215
Quando la seconda frase del pertinente articolo di Wikipedia contiene la frase "analisi di discesa reciprocamente ricorsiva [1] in un algoritmo di analisi generale dall'alto verso il basso [2] [3] che accoglie l'ambiguità e ha lasciato la ricorsione nel tempo e nello spazio polinomiale", penso è del tutto appropriato chiedere a SO cosa sta succedendo.
Clueless

10
@Clueless: quella frase è preceduta da "La memorizzazione è stata utilizzata anche in altri contesti (e per scopi diversi dai guadagni di velocità), come in". Quindi è solo un elenco di esempi (e non è necessario che siano compresi); non fa parte della spiegazione della memoizzazione.
ShreevatsaR

1
@StefanGruenwald Quel link è morto. Potete per favore trovare un aggiornamento?
JS.

2
Nuovo link al file pdf, poiché pycogsci.info non è attivo
Stefan Gruenwald

4
@Clueless, l'articolo in realtà dice " semplice analisi di discesa reciprocamente ricorsiva [1] in un algoritmo di analisi generale dall'alto verso il basso [2] [3] che accoglie l'ambiguità e lascia la ricorsione nel tempo e nello spazio polinomiale". Ti sei perso il semplice , il che ovviamente rende questo esempio molto più chiaro :).
Studgeek,

Risposte:


353

La memorizzazione si riferisce effettivamente al ricordare ("memoization" → "memorandum" → da ricordare) i risultati delle chiamate di metodo basate sugli input del metodo e quindi restituire il risultato memorizzato piuttosto che calcolare nuovamente il risultato. Puoi considerarlo come una cache per i risultati del metodo. Per ulteriori dettagli, vedere a pagina 387 per la definizione in Introduzione agli algoritmi (3e), Cormen et al.

Un semplice esempio di calcolo dei fattoriali che utilizzano la memoizzazione in Python potrebbe essere qualcosa del genere:

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]

Puoi diventare più complicato e incapsulare il processo di memorizzazione in una 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]

Poi:

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

factorial = Memoize(factorial)

Una funzionalità nota come " decoratori " è stata aggiunta in Python 2.4 che ora consente di scrivere semplicemente quanto segue per ottenere lo stesso risultato:

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

La libreria di decoratori Python ha un decoratore simile chiamato memoizedche è leggermente più robusto della Memoizeclasse mostrata qui.


2
Grazie per questo suggerimento La classe Memoize è una soluzione elegante che può essere facilmente applicata al codice esistente senza bisogno di molti refactoring.
Capitano Lepton,

10
La soluzione di classe Memoize è buggy, non funzionerà allo stesso modo di factorial_memo, perché l' factorialinterno def factorialchiama ancora il vecchio immacolato factorial.
Adamsmith,

9
A proposito, puoi anche scrivere if k not in factorial_memo:, che legge meglio di if not k in factorial_memo:.
ShreevatsaR

5
Dovrei davvero farlo come decoratore.
Emlyn O'Regan,

3
@ durden2.0 So che questo è un vecchio commento, ma argsè una tupla. def some_function(*args)rende args una tupla.
Adam Smith,

232

La novità di Python 3.2 è functools.lru_cache. Per impostazione predefinita, memorizza solo nella cache le 128 chiamate utilizzate più di recente, ma è possibile impostare maxsizesu Noneper indicare che la cache non deve mai scadere:

import functools

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

Questa funzione da sola è molto lenta, prova fib(36)e dovrai aspettare circa dieci secondi.

L'aggiunta di lru_cacheannotazioni assicura che se la funzione è stata chiamata di recente per un determinato valore, non ricalcolerà quel valore, ma utilizzerà un risultato precedente memorizzato nella cache. In questo caso, porta a un enorme miglioramento della velocità, mentre il codice non è ingombro di dettagli della cache.


2
Ho provato fib (1000), ottenuto RecursionError: superata la profondità massima di ricorsione in confronto
X Æ A-12

5
@Andyk Il limite di ricorsione predefinito di Py3 è 1000. La prima volta che fibviene chiamato, sarà necessario ricorrere al case base prima che possa avvenire la memoization. Quindi, il tuo comportamento è quasi previsto.
Quelklef,

1
Se non sbaglio, viene memorizzato nella cache solo fino a quando il processo non viene interrotto, giusto? O memorizza nella cache indipendentemente dal fatto che il processo venga interrotto? Ad esempio, suppongo di riavviare il mio sistema: i risultati memorizzati nella cache verranno comunque memorizzati nella cache?
Kristada673,

1
@ Kristada673 Sì, è memorizzato nella memoria del processo, non sul disco.
Flimm,

2
Si noti che ciò accelera anche la prima esecuzione della funzione, poiché è una funzione ricorsiva e sta memorizzando nella cache i propri risultati intermedi. Potrebbe essere utile illustrare una funzione non ricorsiva che è intrinsecamente lenta per rendere più chiaro ai manichini come me. : D
endolith

61

Le altre risposte riguardano ciò che sta abbastanza bene. Non lo sto ripetendo. Solo alcuni punti che potrebbero esserti utili.

Di solito, la memoria è un'operazione che puoi applicare a qualsiasi funzione che calcola qualcosa (costoso) e restituisce un valore. Per questo motivo, è spesso implementato come decoratore . L'implementazione è semplice e sarebbe qualcosa del genere

memoised_function = memoise(actual_function)

o espresso come decoratore

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

18

La memoizzazione mantiene i risultati di calcoli costosi e restituisce il risultato memorizzato nella cache anziché ricalcolarlo continuamente.

Ecco un esempio:

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

Una descrizione più completa è disponibile nella voce Wikipedia sulla memoizzazione .


Hmm, ora, se fosse corretto Python, oscillerebbe, ma sembra non essere ... okay, quindi "cache" non è un dict? Perché se lo è, dovrebbe essere if input not in self.cache e self.cache[input] ( has_keyè obsoleto poiché ... all'inizio della serie 2.x, se non 2.0. self.cache(index)Non è mai stato corretto. IIRC)
Jürgen A. Erhard

15

Non dimentichiamo la hasattrfunzione integrata, per coloro che vogliono fabbricare a mano. In questo modo è possibile mantenere la cache mem all'interno della definizione della funzione (al contrario di una globale).

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]

Sembra un'idea molto costosa. Per ogni n, non solo memorizza nella cache i risultati per n, ma anche per 2 ... n-1.
codeforester,

15

L'ho trovato estremamente 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)

Vedi docs.python.org/3/library/functools.html#functools.wraps per il motivo per cui si dovrebbe usare functools.wraps.
anishpatel,

1
Devo cancellare manualmente per liberare memomemoria?
nn.

L'idea è che i risultati sono memorizzati nel memo all'interno di una sessione. Cioè nulla viene chiarito così com'è
mr.bjerre

6

La memorizzazione consiste sostanzialmente nel salvare i risultati delle operazioni passate eseguite con algoritmi ricorsivi al fine di ridurre la necessità di attraversare l'albero di ricorsione se lo stesso calcolo è richiesto in una fase successiva.

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

Esempio di memoization di Fibonacci in 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]

2
Per maggiori prestazioni pre-seeding il tuo fibcache con i primi pochi valori noti, puoi prendere la logica in più per gestirli dal "percorso attivo" del codice.
jkflying,

5

La memorizzazione è la conversione di funzioni in strutture di dati. Di solito si vuole che la conversione avvenga in modo incrementale e pigramente (su richiesta di un dato elemento di dominio - o "chiave"). Nei linguaggi funzionali pigri, questa conversione pigra può avvenire automaticamente e quindi la memoizzazione può essere implementata senza effetti collaterali (espliciti).


5

Bene, dovrei rispondere prima alla prima parte: che cos'è la memoizzazione?

È solo un metodo per scambiare memoria per tempo. Pensa alla tabella di moltiplicazione .

L'uso di oggetti mutabili come valore predefinito in Python è generalmente considerato errato. Ma se lo usi saggiamente, può effettivamente essere utile implementare a memoization.

Ecco un esempio adattato da http://docs.python.org/2/faq/design.html#why-are-default-values-shared-between-objects

Utilizzando un parametro modificabile dictnella definizione della funzione, è possibile memorizzare nella cache i risultati calcolati intermedi (ad es. Quando si calcola factorial(10)dopo il calcolo factorial(9), è possibile riutilizzare tutti i risultati intermedi)

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

4

Ecco una soluzione che funzionerà con argomenti di tipo list o dict senza lamentarsi:

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

Si noti che questo approccio può essere naturalmente esteso a qualsiasi oggetto implementando la propria funzione hash come caso speciale in handle_item. Ad esempio, per far funzionare questo approccio per una funzione che accetta un set come argomento di input, è possibile aggiungere a handle_item:

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

1
Bel tentativo. Senza lamentarsi, un listargomento di [1, 2, 3]può erroneamente essere considerato lo stesso di un setargomento diverso con un valore di {1, 2, 3}. Inoltre, i set non sono ordinati come dizionari, quindi dovrebbero esserlo anche sorted(). Si noti inoltre che un argomento ricorsivo sulla struttura dei dati causerebbe un ciclo infinito.
martineau,

Sì, i set devono essere gestiti da un involucro speciale handle_item (x) e ordinamento. Non avrei dovuto dire che questa implementazione gestisce gli insiemi, perché non lo fa - ma il punto è che può essere facilmente esteso per farlo tramite un involucro speciale handle_item, e lo stesso funzionerà per qualsiasi classe o oggetto iterabile fintanto che sei disposto a scrivere tu stesso la funzione hash. La parte difficile - trattare con elenchi o dizionari multidimensionali - è già stata affrontata qui, quindi ho scoperto che questa funzione di memoize è molto più semplice da utilizzare come base rispetto ai semplici tipi "I have only hashable argomenti".
Russell

Il problema che ho citato è dovuto al fatto che lists e sets sono "tuple" nella stessa cosa e quindi diventano indistinguibili l'uno dall'altro. Il codice di esempio per l'aggiunta del supporto setsdescritto nell'ultimo aggiornamento non evita che temo. Questo può essere facilmente visto passando separatamente [1,2,3]e {1,2,3}come argomento per una funzione di test "memoize" e vedendo se viene chiamato due volte, come dovrebbe essere o no.
martineau,

sì, ho letto quel problema, ma non l'ho affrontato perché penso che sia molto più lieve dell'altro che hai menzionato. Quando è stata l'ultima volta che hai scritto una funzione memorizzata in cui un argomento fisso può essere un elenco o un set, e i due hanno prodotto output diversi? Se dovessi imbatterti in un caso così raro, riscriveresti semplicemente handle_item per anteporre, dire uno 0 se l'elemento è un set o un 1 se è un elenco.
RussellStewart,

In realtà, c'è un problema simile con lists e dicts perché è possibile che lista abbia esattamente la stessa cosa risultante dalla richiesta make_tuple(sorted(x.items()))di un dizionario. Una soluzione semplice per entrambi i casi sarebbe quella di includere il type()valore nella tupla generata. Mi viene in mente un modo ancora più semplice di gestire sets, ma non generalizza.
martineau,

3

Soluzione che funziona con argomenti posizionali e parole chiave indipendentemente dall'ordine in cui sono stati passati gli argomenti parole chiave (utilizzando 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

Domanda simile: l' identificazione della funzione varargs equivalente richiede la memoizzazione in Python


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]

4
potresti usare semplicemente if n not in cacheinvece. l'utilizzo cache.keyscreerebbe un elenco non necessario in Python 2
n611x007,

2

Volevo solo aggiungere alle risposte già fornite, la libreria di decoratori di Python ha alcune implementazioni semplici ma utili che possono anche memorizzare "tipi non lavabili", a differenza functools.lru_cache.


1
Questo decoratore non memorizza "tipi non lavabili" ! Si limita a chiamare la funzione senza memoizzazione, andare contro l' esplicito è meglio del dogma implicito .
ostrokach,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.