2012-02-26 17 views
6

Sto provando a fare in modo che un oggetto si comporti come un list integrato, tranne per il fatto che il suo valore viene salvato una volta modificato.Come implementare un `elenco` Python persistente?

L'implementazione che arrivo è il wrapping di list in una classe PersistentList. Per ogni accesso al metodo che può modificare l'elenco, il wrapper delega al pacchetto list e lo salva in un database valore-chiave dopo che è stato richiamato.

Codice:

class PersistentList(object): 
    def __init__(self, key): 
     self.key = key 
     self._list = db.get(key, []) 

    def __getattr__(self, name): 
     attr = getattr(self._list, name) 
     if attr: 
      if attr in ('append', 'extend', 'insert', 'pop', 
       'remove', 'reverse', 'sort'): 
       attr = self._autosave(attr) 
      return attr 
     raise AttributeError 

    def _autosave(self, func): 
     @wraps(func) 
     def _(*args, **kwargs): 
      ret = func(*args, **kwargs) 
      self._save() 
      return ret 
     return _ 

    def _save(self): 
     db.set(self.key, self._list) 

Ci sono diversi problemi con questa implementazione:

  1. devo decorare metodi come append ogni volta che sono accesso, c'è un modo migliore per decorare multipla metodi di alcuni oggetti ?

  2. Operazioni come l += [1,2,3] non funzionano perché non ho implementato il metodo IADD.

Cosa posso fare per semplificare questo?

+0

Che cosa succede se uno dei metodi delle liste si chiama solleva un'eccezione? Vuoi ancora salvare? La tua attuale soluzione lo fa ancora ... –

risposta

4

mi piace la risposta di @andrew Cooke, ma non vedo alcun motivo per cui non è possibile derivare direttamente da una lista.

class PersistentList(list): 
    def __init__(self, *args, **kwargs): 
     for attr in ('append', 'extend', 'insert', 'pop', 'remove', 'reverse', 'sort'): 
      setattr(self, attr, self._autosave(getattr(self, attr)) 
     list.__init__(self, *args, **kwargs) 
    def _autosave(self, func): 
     @wraps(func) 
     def _func(*args, **kwargs): 
      ret = func(*args, **kwargs) 
      self._save() 
      return ret 
     return _func 
+0

sì, è meglio. –

+0

Che aspetto ha il tuo '_save'? E come si carica l'oggetto? I miei ingenui tentativi di farlo usando 'pickle' non funzionano. 'pickle.dumps (self)' non funziona mentre 'pickle.dumps (list (self))' fa. O semplicemente convertiresti in una lista ogni volta che '_save' runs()? – kuzzooroo

+0

Inoltre, cosa ti ha reso sicuro di non dover includere "__delitem__", "__delslice__", "__iadd__", "__imul__", "__reversed__", "__setitem__", "__setslice __" nella tua lista di mutatori? – kuzzooroo

0

io so che non è bella o intelligente, ma vorrei solo scrivere i singoli metodi fuori ...

class PersistentList(object): 
    ... 

    def append(self, o): 
     self._autosave() 
     self._list.append(o) 

    ...etc... 
3

Ecco un modo per evitare di dover decorare ogni metodo di elenco. Rende PersistentList un context manager, in modo da poter utilizzare la sintassi

with PersistentList('key', db) as persistent: 
    do_stuff() 

. Certamente, ciò non causa il richiamo del metodo _save dopo ogni operazione di lista, solo quando si esce da with-block. Ma penso che ti dia abbastanza controllo da salvare quando vuoi salvare, specialmente dal momento che il metodo __exit__ è garantito per essere eseguito indipendentemente da come lasci lo with-block, incluso se succede a causa di un'eccezione.

Potrebbe essere un vantaggio che _save non viene chiamato dopo ogni operazione di elenco. Immagina di aggiungerlo alla lista 10.000 volte. Così tante chiamate individuali a db.set (un database?) Potrebbero richiedere parecchio tempo. Sarei migliore, almeno dal punto di vista delle prestazioni, per rendere tutti gli allegati e il salvataggio una volta.


class PersistentList(list): 
    def __init__(self, key, db): 
     self.key = key 
     self.extend(db.get(key, [])) 
    def _save(self): 
     # db.set(self.key, self) 
     print('saving {x}'.format(x = self)) 
    def __enter__(self): 
     return self 
    def __exit__(self,ext_type,exc_value,traceback): 
     self._save() 

db = {} 
p = PersistentList('key', db) 

with p: 
    p.append(1) 
    p.append(2) 

with p: 
    p.pop() 
    p += [1,2,3] 

# saving [1, 2] 
# saving [1, 1, 2, 3] 
+0

Se lo si desidera, è possibile ottenere ancora più fantasia mescolando le due tecniche in modo da mantenere un indicatore "sporco" che indica che è necessario salvarlo. Si potrebbe persino fare in modo che 'PersistenList .__ del__' si lamentasse o provasse a salvare (se il sistema sta uscendo potrebbe fallire) se fosse sporco. –

+0

@ChrisMorgan: mi piace la tua idea, ma penso che sarebbe difficile da implementare correttamente. Ad esempio, se un utente dovesse 'append' poi' pop', un'implementazione ingenua (decorando ciascun metodo di lista) imposterà erroneamente il flag 'dirty'. Per fare meglio, avresti bisogno di salvare una copia della lista in '__enter__' e in ogni test del metodo di lista se la lista è sporca. Tutti questi confronti potrebbero rallentare la performance. Visto che in generale vorresti risparmiare, forse è meglio essere un po 'dispendiosi e salvare ogni volta. – unutbu

+0

Lo inserisco solo come indicatore di base che le cose sono state cambiate. Certo, i cambiamenti potrebbero essere stati annullati, ma come dici tu, il costo di evitare una scrittura non necessaria sarebbe troppo alto. –

0

Ecco una risposta che è molto simile a @ unutbu di, ma più generale. Ti dà una funzione che puoi chiamare per sincronizzare il tuo oggetto su disco, e funziona con altre classi in grado di decollare oltre a list.

with pickle_wrap(os.path.expanduser("~/Desktop/simple_list"), list) as (lst, lst_sync): 
    lst.append("spam") 
    lst_sync() 
    lst.append("ham") 
    print(str(lst)) 
    # lst is synced one last time by __exit__ 

Ecco il codice che rende possibile:

import contextlib, pickle, os, warnings 

def touch_new(filepath): 
    "Will fail if file already exists, or if relevant directories don't already exist" 
    # http://stackoverflow.com/a/1348073/2829764 
    os.close(os.open(filepath, os.O_WRONLY | os.O_CREAT | os.O_EXCL)) 

@contextlib.contextmanager 
def pickle_wrap(filepath, make_new, check_type=True): 
    "Context manager that loads a file using pickle and then dumps it back out in __exit__" 
    try: 
     with open(filepath, "rb") as ifile: 
      result = pickle.load(ifile) 
     if check_type: 
      new_instance = make_new() 
      if new_instance.__class__ != result.__class__: 
       # We don't even allow one class to be a subclass of the other 
       raise TypeError(("Class {} of loaded file does not match class {} of " 
        + "value returned by make_new()") 
        .format(result.__class__, new_instance.__class__)) 
    except IOError: 
     touch_new(filepath) 
     result = make_new() 
    try: 
     hash(result) 
    except TypeError: 
     pass 
    else: 
     warnings.warn("You probably don't want to use pickle_wrap on a hashable (and therefore likely immutable) type") 

    def sync(): 
     print("pickle_wrap syncing") 
     with open(filepath, "wb") as ofile: 
      pickle.dump(result, ofile) 

    yield result, sync 
    sync() 
Problemi correlati