Ho appena avviato Python e non ho idea di cosa sia la memoizzazione e come utilizzarla. Inoltre, posso avere un esempio semplificato?
Ho appena avviato Python e non ho idea di cosa sia la memoizzazione e come utilizzarla. Inoltre, posso avere un esempio semplificato?
Risposte:
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 memoized
che è leggermente più robusto della Memoize
classe mostrata qui.
factorial_memo
, perché l' factorial
interno def factorial
chiama ancora il vecchio immacolato factorial
.
if k not in factorial_memo:
, che legge meglio di if not k in factorial_memo:
.
args
è una tupla. def some_function(*args)
rende args una tupla.
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 maxsize
su None
per 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_cache
annotazioni 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.
fib
viene chiamato, sarà necessario ricorrere al case base prima che possa avvenire la memoization. Quindi, il tuo comportamento è quasi previsto.
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
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 .
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)
Non dimentichiamo la hasattr
funzione 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]
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)
functools.wraps
.
memo
memoria?
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]
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).
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 dict
nella 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]
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)))
list
argomento di [1, 2, 3]
può erroneamente essere considerato lo stesso di un set
argomento 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.
list
s e set
s sono "tuple" nella stessa cosa e quindi diventano indistinguibili l'uno dall'altro. Il codice di esempio per l'aggiunta del supporto sets
descritto 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.
list
s e dict
s perché è possibile che list
a 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 set
s, ma non generalizza.
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
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]
if n not in cache
invece. l'utilizzo cache.keys
creerebbe un elenco non necessario in Python 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
.