2012-02-16 18 views
10

Ho una funzione costosa che accetta e restituisce una piccola quantità di dati (alcuni interi e float). Ho già memoized questa funzione, ma mi piacerebbe rendere il memo persistente. Ci sono già un paio di discussioni relative a questo, ma sono sicuro di potenziali problemi con alcuni degli approcci proposti, e ho alcuni requisiti piuttosto specifici:Memoizzazione persistente in Python

  • Io sicuramente utilizzare la funzione da più thread e processi contemporaneamente (sia utilizzando multiprocessing e da script python separati)
  • non avrò bisogno di leggere o scrivere l'accesso alla nota al di fuori di questa funzione pitone
  • non sono così preoccupati per l'appunto essere corrotto in rare occasioni (come tirare la spina o scrivere accidentalmente sul file senza bloccarlo) in quanto non è che costoso da ricostruire (in genere 10-20 minuti) ma preferirei se non fosse danneggiato a causa di eccezioni, o terminando manualmente un processo python (non so quanto sia realistico)
  • Preferisco fortemente soluzioni che non richiedono grandi librerie esterne in quanto ho una quantità limitata di spazio su disco su un unico computer eseguirò il codice su
  • Ho una debole preferenza per il codice multipiattaforma, ma probabilmente userò solo questo su Linux

This thread discute il modulo shelve, che apparentemente non è sicuro per il processo. Due delle risposte suggeriscono di utilizzare fcntl.flock per bloccare il file shelve. Alcune delle risposte in this thread, tuttavia, sembrano suggerire che questo è pieno di problemi - ma non sono esattamente sicuro di cosa siano. Sembra che questo sia limitato a Unix (anche se apparentemente Windows ha un equivalente chiamato msvcrt.locking), e il blocco è solo "advisory" - cioè, non mi impedirà di scrivere accidentalmente sul file senza averlo controllato. Ci sono altri potenziali problemi? Scrivere in una copia del file e sostituire la copia master come passaggio finale, ridurre il rischio di corruzione?

Non sembra che lo dbm module farà meglio di accantonare. Ho dato uno sguardo veloce a sqlite3, ma sembra un po 'eccessivo per questo scopo. This thread e this one menzionano diverse librerie di terze parti, tra cui ZODB, ma ci sono molte scelte e sembrano tutte eccessivamente grandi e complicate per questo compito.

Qualcuno ha qualche consiglio?

UPDATE: kindall citato IncPy di ​​seguito, che sembra molto interessante. Sfortunatamente, non vorrei tornare a Python 2.6 (attualmente sto usando 3.2), e sembra che sia un po 'scomodo da usare con le librerie C (io uso pesantemente di numpy e scipy, tra gli altri).

L'altra idea di kindall è istruttiva, ma ritengo che adattarla a più processi sarebbe un po 'difficile - suppongo che sarebbe più semplice sostituire la coda con il blocco dei file o un database.

Guardando di nuovo a ZODB, sembra perfetto per l'attività, ma io davvero voglio evitare di usare altre librerie aggiuntive. Non sono ancora del tutto sicuro di quali siano i problemi con il semplice utilizzo di flock - Immagino che un grosso problema sia se un processo viene terminato mentre si scrive sul file o prima di rilasciare il blocco?

Quindi, ho preso il consiglio di synthesizerpatel e sono andato con sqlite3. Se qualcuno è interessato, ho deciso di fare un rimpiazzo in sostituzione di dict che memorizza le sue voci come sottaceti in un database (non mi preoccupo di conservarle in memoria come accesso al database e il pickling è abbastanza veloce rispetto a qualsiasi altra cosa io sia facendo). Sono sicuro che ci sono modi più efficaci per farlo (e non ho idea se potrei ancora avere problemi di concorrenza), ma qui è il codice:

from collections import MutableMapping 
import sqlite3 
import pickle 


class PersistentDict(MutableMapping): 
    def __init__(self, dbpath, iterable=None, **kwargs): 
     self.dbpath = dbpath 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'create table if not exists memo ' 
       '(key blob primary key not null, value blob not null)' 
      ) 
     if iterable is not None: 
      self.update(iterable) 
     self.update(kwargs) 

    def encode(self, obj): 
     return pickle.dumps(obj) 

    def decode(self, blob): 
     return pickle.loads(blob) 

    def get_connection(self): 
     return sqlite3.connect(self.dbpath) 

    def __getitem__(self, key): 
     key = self.encode(key) 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select value from memo where key=?', 
       (key,) 
      ) 
      value = cursor.fetchone() 
     if value is None: 
      raise KeyError(key) 
     return self.decode(value[0]) 

    def __setitem__(self, key, value): 
     key = self.encode(key) 
     value = self.encode(value) 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'insert or replace into memo values (?, ?)', 
       (key, value) 
      ) 

    def __delitem__(self, key): 
     key = self.encode(key) 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select count(*) from memo where key=?', 
       (key,) 
      ) 
      if cursor.fetchone()[0] == 0: 
       raise KeyError(key) 
      cursor.execute(
       'delete from memo where key=?', 
       (key,) 
      ) 

    def __iter__(self): 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select key from memo' 
      ) 
      records = cursor.fetchall() 
     for r in records: 
      yield self.decode(r[0]) 

    def __len__(self): 
     with self.get_connection() as connection: 
      cursor = connection.cursor() 
      cursor.execute(
       'select count(*) from memo' 
      ) 
      return cursor.fetchone()[0] 
+2

Se è possibile gestire Python 2.6.3 e non è su Windows, si potrebbe voler controllare [IncPy] (http://www.stanford.edu/~pgbovine/incpy.html) che verrà automaticamente e memorizza costantemente il tuo * intero programma * ovunque sia sicuro farlo. – kindall

risposta

7

sqlite3 fuori dalla scatola fornisce ACID. Il blocco dei file è soggetto a condizioni di competizione e problemi di concorrenza che non si avranno utilizzando sqlite3.

Fondamentalmente, sì, sqlite3 è più di quello che ti serve, ma non è un onere enorme. Può funzionare su telefoni cellulari, quindi non è come se ti stessi impegnando a far funzionare un software bestiale. Ti farà risparmiare tempo reinventando le ruote e risolvendo i problemi di blocco.

6

Presumo che si desideri continuare a memoizzare i risultati della funzione nella RAM, probabilmente in un dizionario, ma utilizzare la persistenza per ridurre il tempo di "riscaldamento" dell'applicazione. In questo caso non accederai in modo casuale agli articoli direttamente nel backing store, quindi un database potrebbe effettivamente essere eccessivo (anche se come note synthesizerpatel, forse non tanto quanto pensi).

Tuttavia, se si desidera eseguire il rollover, una strategia praticabile potrebbe essere semplicemente caricare il dizionario da un file all'inizio della corsa prima di iniziare qualsiasi thread. Quando un risultato non è nel dizionario, è necessario scriverlo sul file dopo averlo aggiunto al dizionario. È possibile farlo aggiungendolo a una coda e utilizzando un singolo thread di lavoro che svuota gli elementi dalla coda al disco (basta accodarli a un singolo file andrebbe bene). A volte potresti aggiungere lo stesso risultato più di una volta, ma ciò non è fatale poiché ogni volta sarà lo stesso risultato, quindi rileggerlo due o più volte non arrecherà alcun danno reale. Il modello di threading di Python ti terrà fuori dalla maggior parte dei problemi di concorrenza (ad es., L'aggiunta ad una lista è atomica).

Ecco alcune (generiche incompleta non testato,,) il codice che mostra di cosa sto parlando:

import cPickle as pickle 

import time, os.path 

cache = {} 
queue = [] 

# run at script start to warm up cache 
def preload_cache(filename): 
    if os.path.isfile(filename): 
     with open(filename, "rb") as f: 
      while True: 
       try: 
        key, value = pickle.load(f), pickle.load(f) 
       except EOFError: 
        break 
       cache[key] = value 

# your memoized function 
def time_consuming_function(a, b, c, d): 
    key = (a, b, c, d) 
    if key in cache: 
     return cache[key] 
    else: 
     # generate the result here 
     # ... 
     # add to cache, checking to see if it's already there again to avoid writing 
     # it twice (in case another thread also added it) (this is not fatal, though) 
     if key not in cache: 
      cache[key] = result 
      queue.append((key, result)) 
     return result 

# run on worker thread to write new items out 
def write_cache(filename): 
    with open(filename, "ab") as f: 
     while True: 
      while queue: 
       key, value = queue.pop() # item order not important 
       # but must write key and value in single call to ensure 
       # both get written (otherwise, interrupting script might 
       # leave only one written, corrupting the file) 
       f.write(pickle.dumps(key, pickle.HIGHEST_PROTOCOL) + 
         pickle.dumps(value, pickle.HIGHEST_PROTOCOL)) 
      f.flush() 
      time.sleep(1) 

Se ho avuto il tempo, mi piacerebbe trasformare questo in un decoratore ... e mettere la persistenza in una sottoclasse dict ... anche l'uso di variabili globali è sub-ottimale. :-) Se utilizzi questo approccio con multiprocessing probabilmente vorrai usare uno multiprocessing.Queue piuttosto che un elenco; è quindi possibile utilizzare queue.get() come attesa di blocco per un nuovo risultato nel processo di lavoro che scrive sul file. Non ho usato multiprocessing, quindi, prendi questo po 'di consiglio con un pizzico di sale.