2012-02-16 13 views
10

Tengo una función costosa que toma y devuelve una pequeña cantidad de datos (algunos enteros y flotantes). Ya tengo memoized esta función, pero me gustaría que la nota sea persistente. Ya hay un par de hilos relacionados con este, pero estoy seguro acerca de los posibles problemas con algunos de los enfoques sugeridos, y tengo algunos requisitos bastante específicos:Memorización persistente en Python

  • Definitivamente voy a utilizar la función de múltiples hilos y procesos simultáneamente (tanto utilizando multiprocessing y de scripts python separadas)
  • no necesitaré de lectura o escritura a la nota desde fuera esta función pitón
  • no estoy tan preocupado por el memorándum se corrompan en raras ocasiones (como tirar el enchufe o accidentalmente escribiendo en el archivo sin bloquearlo) ya que no es que costoso de reconstruir (generalmente 10-20 minutos) pero preferiría que no se corrompiera debido a excepciones, o terminar manualmente un proceso de python (no sé qué tan realista es)
  • Preferiría las soluciones que no requieren grandes librerías externas ya que tengo una cantidad de espacio en disco rígidamente limitada en una máquina. Ejecutaré el código en
  • Tengo una preferencia débil por el código multiplataforma, pero probablemente solo use esto. en Linux

This thread discute el módulo shelve, que al parecer no es un proceso seguro. Dos de las respuestas sugieren usar fcntl.flock para bloquear el archivo de archivado. Algunas de las respuestas en this thread, sin embargo, parecen sugerir que esto está plagado de problemas, pero no estoy exactamente seguro de lo que son. Suena como si esto estuviera limitado a Unix (aunque aparentemente Windows tiene un equivalente llamado msvcrt.locking), y el bloqueo es solo 'de advertencia', es decir, no me impedirá escribir accidentalmente en el archivo sin verificar que esté bloqueado. ¿Hay algún otro problema potencial? ¿Escribir en una copia del archivo y reemplazar la copia maestra como paso final reduce el riesgo de corrupción?

No parece que dbm module sea mejor que dejar de lado. He echado un vistazo rápido al sqlite3, pero parece un poco exagerado para este propósito. This thread y this one mencionan varias bibliotecas de terceros, incluido ZODB, pero hay muchas opciones, y todas parecen demasiado grandes y complicadas para esta tarea.

¿Alguien tiene algún consejo?

ACTUALIZACIÓN: kindall mencionó IncPy a continuación, que se ve muy interesante. Desafortunadamente, no me gustaría volver a Python 2.6 (en realidad estoy usando 3.2), y parece que es un poco incómodo de usar con las bibliotecas C (hago un uso intensivo de numpy y skipy, entre otros).

La otra idea de kindall es instructiva, pero creo que adaptar esto a múltiples procesos sería un poco difícil, supongo que sería más fácil reemplazar la cola con bloqueo de archivos o una base de datos.

Al volver a mirar ZODB, parece perfecto para la tarea, pero realmente quiero evitar el uso de bibliotecas adicionales. Todavía no estoy del todo seguro de cuáles son los problemas con simplemente usar flock - Me imagino que un gran problema es si un proceso finaliza al escribir en el archivo o antes de liberar el bloqueo.

Por lo tanto, he tomado el consejo de synthesizerpatel y me he ido con sqlite3. Si alguien está interesado, decidí hacer un reemplazo directo para dict que almacena sus entradas como pepinillos en una base de datos (no me molesto en guardar ninguno en la memoria ya que el acceso a la base de datos y el encurtido son lo suficientemente rápidos en comparación con todo lo demás) obra). Estoy seguro de que hay formas más eficientes de hacer esto (y no tengo ni idea de si todavía podría tener problemas de concurrencia), pero aquí está el código:

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

Si puede ocuparse de Python 2.6.3 y no está en Windows, es posible que desee comprobar [IncPy] (http://www.stanford.edu/~pgbovine/incpy.html) que automáticamente y memorice persistentemente su * programa completo * donde sea seguro hacerlo. – kindall

Respuesta

7

sqlite3 fuera de la caja ofrece ACID. El bloqueo de archivos es propenso a condiciones de carrera y problemas de concurrencia que no tendrá con sqlite3.

Básicamente, sí, sqlite3 es más de lo que necesita, pero no es una gran carga. Se puede ejecutar en teléfonos móviles, por lo que no es como si te comprometieras a ejecutar algún software bestial. Le ahorrará tiempo reinventando ruedas y solucionando problemas de bloqueo.

6

Supongo que desea continuar memorizando los resultados de la función en la RAM, probablemente en un diccionario, pero use la persistencia para reducir el tiempo de "calentamiento" de la aplicación. En este caso, no va a tener acceso aleatorio a los elementos directamente en la tienda de respaldo, por lo que una base de datos podría ser exagerada (aunque como notas de synthesizerpatel, tal vez no tanto como cree).

Aún así, si quiere hacer su propia estrategia, una estrategia viable podría ser simplemente cargar el diccionario desde un archivo al comienzo de su ejecución antes de iniciar cualquier conversación. Cuando un resultado no está en el diccionario, debe escribirlo en el archivo después de agregarlo al diccionario. Puede hacer esto agregándolo a una cola y utilizando un solo hilo de trabajo que vacía los elementos de la cola en el disco (simplemente adjuntarlos a un solo archivo estaría bien). Puede que de vez en cuando agregue el mismo resultado más de una vez, pero esto no es fatal, ya que será el mismo resultado cada vez, por lo que volver a leerlo dos veces o más no causará ningún daño real. El modelo de subprocesamiento de Python lo mantendrá alejado de la mayoría de los tipos de problemas de concurrencia (por ejemplo, agregar a una lista es atómico).

Aquí hay un código (no probado, genérico, incompleta) muestra lo que estoy hablando:

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) 

Si tuviera tiempo, me gustaría convertir esto en un decorador ... y poner la persistencia en una subclase dict ... el uso de variables globales tampoco es óptimo. :-) Si usa este enfoque con multiprocessing, probablemente quiera usar un multiprocessing.Queue en lugar de una lista; puede usar queue.get() como espera de bloqueo para un nuevo resultado en el proceso de trabajo que escribe en el archivo. No he usado multiprocessing, así que tome este consejo con un grano de sal.

Cuestiones relacionadas