2012-06-06 12 views
6

Quindi di recente ho fatto una domanda sulla memoizzazione e ho ottenuto ottime risposte, e ora voglio portarlo al livello successivo. Dopo un po 'di ricerca su google, non sono riuscito a trovare un'implementazione di riferimento di un decoratore memoize che fosse in grado di memorizzare nella cache una funzione che richiedeva argomenti per le parole chiave. In effetti, la maggior parte di essi utilizzava semplicemente *args come chiave per la ricerca della cache, il che significa che si interromperebbe anche se si desidera memorizzare una funzione che accetta liste o dict come argomenti.Esiste un modo pitonico per supportare gli argomenti delle parole chiave per un decoratore memoize in Python?

Nel mio caso, il primo argomento della funzione è un identificatore univoco di per sé, adatto per l'uso come chiave dict per le ricerche della cache, tuttavia volevo la possibilità di utilizzare gli argomenti delle parole chiave e accedere comunque alla stessa cache. Quello che intendo è, my_func('unique_id', 10) e my_func(foo=10, func_id='unique_id'), se entrambi devono restituire lo stesso risultato memorizzato nella cache.

Per fare ciò, ciò di cui abbiamo bisogno è un modo pulito e pitonico di dire "ispezionare kwargs per qualsiasi parola chiave corrisponde al primo argomento". Questo è quello che mi è venuto in mente:

class memoize(object): 
    def __init__(self, cls): 
     if type(cls) is FunctionType: 
      # Let's just pretend that the function you gave us is a class. 
      cls.instances = {} 
      cls.__init__ = cls 
     self.cls = cls 
     self.__dict__.update(cls.__dict__) 

    def __call__(self, *args, **kwargs): 
     """Return a cached instance of the appropriate class if it exists.""" 
     # This is some dark magic we're using here, but it's how we discover 
     # that the first argument to Photograph.__init__ is 'filename', but the 
     # first argument to Camera.__init__ is 'camera_id' in a general way. 
     delta = 2 if type(self.cls) is FunctionType else 1 
     first_keyword_arg = [k 
      for k, v in inspect.getcallargs(
       self.cls.__init__, 
       'self', 
       'first argument', 
       *['subsequent args'] * (len(args) + len(kwargs) - delta)).items() 
        if v == 'first argument'][0] 
     key = kwargs.get(first_keyword_arg) or args[0] 
     print key 
     if key not in self.cls.instances: 
      self.cls.instances[key] = self.cls(*args, **kwargs) 
     return self.cls.instances[key] 

La cosa pazzesca è che questo funziona davvero. Ad esempio, se si decorare in questo modo:

@memoize 
class FooBar: 
    instances = {} 

    def __init__(self, unique_id, irrelevant=None): 
     print id(self) 

Poi dal codice è possibile chiamare o FooBar('12345', 20) o FooBar(irrelevant=20, unique_id='12345') e effettivamente ottenere la stessa istanza di FooBar. È quindi possibile definire una classe diversa con un nome diverso per il primo argomento, perché funziona in un modo generale (cioè, il decoratore non ha bisogno di sapere nulla di specifico sulla classe che sta decorando in modo che funzioni).

Il problema è, si tratta di un pasticcio empio ;-)

Funziona perché inspect.getcallargs restituisce un dict mappatura delle parole chiave definite agli argomenti che fornite, così ho fornirla alcuni argomenti fasulli e quindi ispezionare il dict per il primo argomento che è passato.

Cosa sarebbe molto più bello, se una cosa del genere dovesse esistere anche, è un analogo a inspect.getcallargs che ha restituito entrambi i tipi di argomenti unificati come un elenco degli argomenti anziché come un dotto degli argomenti delle parole chiave. Ciò consentirebbe qualcosa di simile:

def __call__(self, *args, **kwargs): 
    key = inspect.getcallargsaslist(self.cls.__init__, None, *args, **kwargs)[1] 
    if key not in self.cls.instances: 
     self.cls.instances[key] = self.cls(*args, **kwargs) 
    return self.cls.instances[key] 

L'altro modo che posso vedere di affrontare questo sarebbe utilizzando il dict fornito dal inspect.getcallargs come la chiave di cache di ricerca direttamente, ma che richiederebbe un modo ripetibile per fare le stringhe identici da hash identici, che è qualcosa che ho sentito non può essere invocato (credo che dovrei costruire la stringa io stesso dopo aver ordinato i tasti).

Qualcuno ha qualche idea su questo? È sbagliato voler chiamare una funzione con argomenti di parole chiave e memorizzare i risultati? O solo molto difficile?

risposta

4

Io suggerirei qualcosa di simile al seguente:

import inspect 

class key_memoized(object): 
    def __init__(self, func): 
     self.func = func 
     self.cache = {} 

    def __call__(self, *args, **kwargs): 
     key = self.key(args, kwargs) 
     if key not in self.cache: 
      self.cache[key] = self.func(*args, **kwargs) 
     return self.cache[key] 

    def normalize_args(self, args, kwargs): 
     spec = inspect.getargs(self.func.__code__).args 
     return dict(kwargs.items() + zip(spec, args)) 

    def key(self, args, kwargs): 
     a = self.normalize_args(args, kwargs) 
     return tuple(sorted(a.items())) 

Esempio:

@key_memoized 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar + baz + spam 

print foo(1, 2, 3) 
print foo(1, 2, spam=3)   #memoized 
print foo(spam=3, baz=2, bar=1) #memoized 

Nota che è anche possibile estendere key_memoized e sovrascrivere il suo metodo key() per fornire strategie di memoizzazione più specifiche , per esempio. di ignorare alcuni degli argomenti:

class memoize_by_bar(key_memoized): 
    def key(self, args, kwargs): 
     return self.normalize_args(args, kwargs)['bar'] 

@memoize_by_bar 
def foo(bar, baz, spam): 
    print 'calling foo: bar=%r baz=%r spam=%r' % (bar, baz, spam) 
    return bar 

print foo('x', 'ignore1', 'ignore2') 
print foo('x', 'ignore3', 'ignore4') 
+0

Mi piace l'aspetto di questo, ma sono preoccupato per la parte in cui 'key' restituisce' tuple (a.items()) '. È garantito per ordinare le chiavi nello stesso ordine per dizioni distinte ma identiche? Ho sentito dire che le dict non sono ordinate e basarsi su cose come 'str ({1: 2,3: 4})' per produrre stringhe ripetibili dato input identici è Molto Molto Errante. – robru

+0

Sembra che 'inspect.getargspec (func) .args [0]' è la risposta precisa alla domanda specifica che ho chiesto (come trovare il nome del primo argomento), ma mi piacerebbe espandere questo in un soluzione più generale. Pubblicherò i miei risultati più tardi una volta che avrò avuto un po 'di tempo per migliorarlo. – robru

+0

@Robru: buon punto per l'ordinamento dei dit. Modificato su 'tuple (sort (a.items()))' (un'altra opzione sarebbe 'frozenset (a.items())'). – georg

3

Prova lru_cache:

@functools.lru_cache(maxsize=128, typed=False)

Decorator per avvolgere una funzione con un callable memoizing che consente di risparmiare fino al maxsize chiamate più recenti. Può far risparmiare tempo quando una funzione costosa o I/O legata viene periodicamente chiamata con gli stessi argomenti.

lru_cache aggiunto in Python 3.2, ma può essere backport in 2.x

+0

interessante leggere, ma purtroppo non funziona nella mia situazione perché ho staticmethods di classe che hanno bisogno di essere in grado di iterare le istanze memorizzate nella cache, in modo che la cache di per sé ha bisogno di essere esposto come un attributo di classe. – robru

Problemi correlati