2012-06-04 7 views
13

Ok, ecco lo scenario del mondo reale: sto scrivendo un'applicazione e ho una classe che rappresenta un certo tipo di file (nel mio caso si tratta di fotografie ma quel dettaglio è irrilevante per il problema). Ogni istanza della classe Photograph deve essere unica al nome del file della foto.Come posso memoizzare un'istanza di classe in Python?

Il problema è che quando un utente dice alla mia applicazione di caricare un file, devo essere in grado di identificare quando i file sono già caricati e utilizzare l'istanza esistente per quel nome file, piuttosto che creare istanze duplicate sullo stesso nome file .

Per me sembra una buona situazione usare la memoizzazione, e ci sono molti esempi di questo, ma in questo caso non sto solo memoizzando una funzione ordinaria, ho bisogno di essere memoizing __init__(). Questo pone un problema, perché quando viene chiamato il numero __init__() è già troppo tardi perché è già stata creata una nuova istanza.

Nella mia ricerca ho trovato il metodo __new__() di Python, e in realtà ero in grado di scrivere un esempio banale di lavoro, ma è andato in pezzi quando ho provato ad usarlo sui miei oggetti del mondo reale, e non sono sicuro del perché (l'unica cosa che riesco a pensare è che i miei oggetti del mondo reale erano sottoclassi di altri oggetti che non sono in grado di controllare realmente, e quindi c'erano alcune incompatibilità con questo approccio). Questo è quello che avevo:

class Flub(object): 
    instances = {} 

    def __new__(cls, flubid): 
     try: 
      self = Flub.instances[flubid] 
     except KeyError: 
      self = Flub.instances[flubid] = super(Flub, cls).__new__(cls) 
      print 'making a new one!' 
      self.flubid = flubid 
     print id(self) 
     return self 

    @staticmethod 
    def destroy_all(): 
     for flub in Flub.instances.values(): 
      print 'killing', flub 


a = Flub('foo') 
b = Flub('foo') 
c = Flub('bar') 

print a 
print b 
print c 
print a is b, b is c 

Flub.destroy_all() 

quale uscita questo:

making a new one! 
139958663753808 
139958663753808 
making a new one! 
139958663753872 
<__main__.Flub object at 0x7f4aaa6fb050> 
<__main__.Flub object at 0x7f4aaa6fb050> 
<__main__.Flub object at 0x7f4aaa6fb090> 
True False 
killing <__main__.Flub object at 0x7f4aaa6fb050> 
killing <__main__.Flub object at 0x7f4aaa6fb090> 

E 'perfetto! Sono state fatte solo due istanze per i due ID univoci, e Flub.instances ne ha solo due elencati.

Ma quando ho provato a utilizzare questo approccio con gli oggetti che stavo usando, ho ricevuto tutti i tipi di errori privi di senso su come __init__() ha preso solo 0 argomenti, non 2. Quindi avrei cambiato alcune cose in giro e poi avrebbe detto me che __init__() aveva bisogno di un argomento. Totalmente bizzarro.

Dopo un po 'di lotta con esso, ho praticamente appena rinunciato e si è trasferito tutta la magia nera __new__() in uno staticmethod chiamato get, in modo tale che potrei chiamare Photograph.get(filename) e sarebbe chiamare solo Photograph(filename) se il nome del file non fosse già in Photograph.instances .

Qualcuno sa dove sono andato storto qui? C'è un modo migliore per farlo?

Un altro modo di pensare a questo proposito è che è simile ad un Singleton, tranne che non è a livello globale Singleton, Singleton solo-per-il nome del file.

Here's my real-world code using the staticmethod get se volete vedere tutto insieme.

+1

Ho modificato la domanda per rimuovere quelle cose che hai detto. – robru

risposta

11

vediamo due punti sulla tua domanda.

Utilizzando Memoize

È possibile utilizzare Memoizzazione, ma si dovrebbe decorare la classe, non il metodo __init__. Supponiamo di avere questo memoizator:

def get_id_tuple(f, args, kwargs, mark=object()): 
    """ 
    Some quick'n'dirty way to generate a unique key for an specific call. 
    """ 
    l = [id(f)] 
    for arg in args: 
     l.append(id(arg)) 
    l.append(id(mark)) 
    for k, v in kwargs: 
     l.append(k) 
     l.append(id(v)) 
    return tuple(l) 

_memoized = {} 
def memoize(f): 
    """ 
    Some basic memoizer 
    """ 
    def memoized(*args, **kwargs): 
     key = get_id_tuple(f, args, kwargs) 
     if key not in _memoized: 
      _memoized[key] = f(*args, **kwargs) 
     return _memoized[key] 
    return memoized 

Ora non vi resta per decorare la classe:

@memoize 
class Test(object): 
    def __init__(self, somevalue): 
     self.somevalue = somevalue 

Vediamo un test?

tests = [Test(1), Test(2), Test(3), Test(2), Test(4)] 
for test in tests: 
    print test.somevalue, id(test) 

L'uscita è di seguito. Si noti che gli stessi parametri producono lo stesso ID dell'oggetto restituito:

1 3072319660 
2 3072319692 
3 3072319724 
2 3072319692 
4 3072319756 

In ogni caso, preferirei creare una funzione per generare gli oggetti e Memoize esso. Sembra più pulita a me, ma può essere un po 'irrilevante piccolo cruccio:

class Test(object): 
    def __init__(self, somevalue): 
     self.somevalue = somevalue 

@memoize 
def get_test_from_value(somevalue): 
    return Test(somevalue) 

Utilizzando __new__:

O, naturalmente, è possibile ignorare __new__. Alcuni giorni fa ho pubblicato an answer about the ins, outs and best practices of overriding __new__ che può essere utile. Fondamentalmente, si dice di passare sempre *args, **kwargs al tuo metodo __new__.

I, per esempio, preferirei memorizzare una funzione che crea gli oggetti, o anche scrivere una funzione specifica di cui farebbe attenzione a non ricreare mai un oggetto con lo stesso parametro. Naturalmente, comunque, questo è principalmente un mio parere, non una regola.

+0

Grazie. Non mi rendevo conto che si poteva mettere il decoratore direttamente sulla classe invece che sui metodi. Quello era il bit chiave di informazioni che mi mancava. Il tuo decoratore di memoize non è * abbastanza * ciò di cui ho bisogno perché le stringhe non sono singleton come i numeri (e quindi 'id's non sono univoci da una stringa identica a un'altra), ma per le mie esigenze semplificate sono stato in grado di usa il primo argomento direttamente come chiave. – robru

+0

@Robru sicuramente il mio memoize è solo un codice veloce che uso negli esempi, non presta molta attenzione :) – brandizzi

+0

Ovviamente, dopo un'ora di rifinitura del tuo decoratore memoize per lavorare con la mia particolare configurazione di classi, mi viene in mente che questa soluzione in realtà non funzionerà perché ho un numero di metodi e funzioni che iterano su 'ClassName.instances' dict per fare operazioni su tutte le istanze caricate, e questa particolare tecnica di memoizzazione mescola tutte le diverse istanze di classi diverse in un unico dettato. Sembra che dopo tutto andrò con '__new__'. – robru

2

I parametri per __new__ anche ottenere passati a __init__, quindi:

def __init__(self, flubid): 
    ... 

è necessario accettare l'argomento flubid lì, anche se non si utilizza in __init__

Ecco l'rilevante comment tratto da typeobject.c in Python2.7.3

/* You may wonder why object.__new__() only complains about arguments 
    when object.__init__() is not overridden, and vice versa. 

    Consider the use cases: 

    1. When neither is overridden, we want to hear complaints about 
     excess (i.e., any) arguments, since their presence could 
     indicate there's a bug. 

    2. When defining an Immutable type, we are likely to override only 
     __new__(), since __init__() is called too late to initialize an 
     Immutable object. Since __new__() defines the signature for the 
     type, it would be a pain to have to override __init__() just to 
     stop it from complaining about excess arguments. 

    3. When defining a Mutable type, we are likely to override only 
     __init__(). So here the converse reasoning applies: we don't 
     want to have to override __new__() just to stop it from 
     complaining. 

    4. When __init__() is overridden, and the subclass __init__() calls 
     object.__init__(), the latter should complain about excess 
     arguments; ditto for __new__(). 

    Use cases 2 and 3 make it unattractive to unconditionally check for 
    excess arguments. The best solution that addresses all four use 
    cases is as follows: __init__() complains about excess arguments 
    unless __new__() is overridden and __init__() is not overridden 
    (IOW, if __init__() is overridden or __new__() is not overridden); 
    symmetrically, __new__() complains about excess arguments unless 
    __init__() is overridden and __new__() is not overridden 
    (IOW, if __new__() is overridden or __init__() is not overridden). 

    However, for backwards compatibility, this breaks too much code. 
    Therefore, in 2.6, we'll *warn* about excess arguments when both 
    methods are overridden; for all other cases we'll use the above 
    rules. 

*/ 
+0

Quello che dici ha senso, ma come funziona il mio banale esempio senza definire '__init__'? Non dovrebbe anche darmi errori sul numero errato di argomenti passati? – robru

+0

@Robru, ho aggiornato la mia risposta con la spiegazione fornita in 'typeobject.c' –

+0

Oh, ok. Grazie. – robru

3

La soluzione che ho finito per usare è questo:

class memoize(object): 
    def __init__(self, cls): 
     self.cls = cls 
     self.__dict__.update(cls.__dict__) 

     # This bit allows staticmethods to work as you would expect. 
     for attr, val in cls.__dict__.items(): 
      if type(val) is staticmethod: 
       self.__dict__[attr] = val.__func__ 

    def __call__(self, *args): 
     key = '//'.join(map(str, args)) 
     if key not in self.cls.instances: 
      self.cls.instances[key] = self.cls(*args) 
     return self.cls.instances[key] 

E poi si decorano la classe con questo, non __init__. Sebbene brandizzi mi abbia fornito quell'informazione chiave, il suo decoratore di esempio non ha funzionato come desiderato.

ho trovato questo concetto abbastanza sottili, ma in fondo quando si utilizza decoratori in Python, è necessario comprendere che la cosa che viene decorato (se si tratta di un metodo o di una classe) è in realtà sostituito da decoratore sé . Ad esempio, quando provavo ad accedere a Photograph.instances o Camera.generate_id() (un metodo statico), non potevo effettivamente accedervi perché Photograph non si riferisce effettivamente alla classe di fotografia originale, si riferisce alla funzione memoized (dall'esempio di brandizzi).

Per aggirare questo problema, ho dovuto creare una classe di decoratore che in effetti avesse preso tutti gli attributi e i metodi statici della classe decorata e li avesse esposti come se fosse di sua proprietà. Quasi come una sottoclasse, tranne per il fatto che la classe decoratrice non sa in anticipo quali classi sarà decorata, quindi deve copiare gli attributi dopo il fatto.

Il risultato finale è che qualsiasi istanza della classe memoize diventa un involucro quasi trasparente attorno alla classe effettiva che ha decorato, con l'eccezione che il tentativo di istanziarlo (ma in realtà chiamandolo) fornirà copie in cache quando sono disponibili.

Problemi correlati