2010-04-26 8 views
23

Tengo una función python que tiene un resultado determinista. Se necesita mucho tiempo para correr y genera una gran salida:Hashing una función python para regenerar la salida cuando se modifica la función

def time_consuming_function(): 
    # lots_of_computing_time to come up with the_result 
    return the_result 

modifico time_consuming_function de vez en cuando, pero me gustaría evitar tener que correr de nuevo, mientras que es sin cambios. [time_consuming_function solo depende de las funciones que son inmutables para los fines considerados aquí; es decir, podría tener funciones de las bibliotecas de Python, pero no de otras partes de mi código que cambiaría.] La solución que me sugiere a mí mismo es almacenar en caché el resultado y también almacenar en caché algún "hash" de la función. Si el hash cambia, la función habrá sido modificada, y tenemos que volver a generar la salida.

¿Es esto posible o ridículo?


Actualizado: basado en las respuestas, parece que lo que quiero hacer es "memoize" time_consuming_function, pero en lugar de (o además de) los argumentos pasado a una función invariante, quiero cuenta para una función que a su vez cambiará.

+4

+1 por un problema interesante. – zdav

+0

¿Cómo se modifica el método? ¿Desea mantener hash en las ejecuciones del programa o está dentro de una ejecución pero en algunas recargas de módulos? – doublep

+0

Tendría el método en un archivo de script; Probablemente lo modificaría a mano de vez en cuando. La aplicación es que esta función generaría "datos de problemas" para ejecutar en algún código de simulación. Cambiaría el problema de vez en cuando. –

Respuesta

6

Si entiendo su problema, creo que lo abordaría de esta manera. Es un toque maligno, pero creo que es más confiable y puntual que las otras soluciones que veo aquí.

import inspect 
import functools 
import json 

def memoize_zeroadic_function_to_disk(memo_filename): 
    def decorator(f): 
     try: 
      with open(memo_filename, 'r') as fp: 
       cache = json.load(fp) 
     except IOError: 
      # file doesn't exist yet 
      cache = {} 

     source = inspect.getsource(f) 

     @functools.wraps(f) 
     def wrapper(): 
      if source not in cache: 
       cache[source] = f() 
       with open(memo_filename, 'w') as fp: 
        json.dump(cache, fp) 

      return cache[source] 
     return wrapper 
    return decorator 

@memoize_zeroadic_function_to_disk(...SOME PATH HERE...) 
def time_consuming_function(): 
    # lots_of_computing_time to come up with the_result 
    return the_result 
+0

Por lo tanto, el único método hash que se ejecuta es el hashing de la clave del diccionario interno de Python, donde la clave es el valor de cadena del código no compilado completo de una función. ¿Hay alguna manera de obtener el código compilado de la función, por lo que cambiar el espacio entre líneas o los comentarios no dará como resultado un valor diferente? –

+0

@Seth, sí, tiene sentido para mí usar el hash interno de Python aquí, ya que lo que realmente quieres es comparar el valor (no sea que tengas colisiones hash y no lo sepas. Esto es poco probable pero muy posible.) Si solo quieres para almacenar en caché la función más reciente, no usaría un dict o hash en absoluto, pero simplemente compare el valor. Solo desde que dijiste (fuera de banda) que querías almacenar varias versiones de la función para poder volver al código anterior, utilicé el dict. –

+0

@Seth, Debería ser posible almacenar menos información que el código fuente completo (espacios en blanco y comentarios intactos como este), pero se tendrá cuidado de que se tenga una condición necesaria y suficiente para una coincidencia. 'f.func_code.co_code' es el código de bytes que la función realmente almacena, pero no estoy seguro de que pueda prometerle que será igual entre las compilaciones o no. Tampoco estoy completamente seguro de que no pueda darte falsos positivos. –

1

En lugar de poner la función en una cadena, colocaría la función en su propio archivo. Llámalo time_consuming.py, por ejemplo. Se vería algo como esto:

def time_consuming_method(): 
    # your existing method here 

# Is the cached data older than this file? 
if (not os.path.exists(data_file_name) 
    or os.stat(data_file_name).st_mtime < os.stat(__file__).st_mtime): 
    data = time_consuming_method() 
    save_data(data_file_name, data) 
else: 
    data = load_data(data_file_name) 

# redefine method 
def time_consuming_method(): 
    return data 

Al probar la infraestructura para que esto funcione, me comente las partes lentas. Haga una función simple que simplemente devuelva 0, obtenga todo el trabajo de guardar/cargar a su gusto, luego vuelva a colocar los bits lentos.

-1

Lo que usted describe es efectivamente memoization. Las funciones más comunes se pueden memorizar definiendo un decorador.

A (excesivamente simplificada) ejemplo:

def memoized(f): 
    cache={} 
    def memo(*args): 
     if args in cache: 
      return cache[args] 
     else: 
      ret=f(*args) 
      cache[args]=ret 
      return ret 
    return memo 

@memoized 
def time_consuming_method(): 
    # lots_of_computing_time to come up with the_result 
    return the_result 

Editar:

Desde el comentario de Mike Graham y actualización de la OP, ahora está claro que los valores tienen que ser almacenado en caché en diferentes ejecuciones del programa. Esto se puede hacer mediante el uso de almacenamiento persistente para la memoria caché (por ejemplo, algo tan simple como usar Pickle o un archivo de texto simple, o tal vez usar una base de datos completa, o cualquier cosa intermedia). La elección de qué método utilizar depende de lo que necesita el OP. Varias otras respuestas ya dan algunas soluciones a esto, así que no voy a repetir eso aquí.

+0

Parece que OP quiere memorizar el valor de retorno de una función entre tiempos de ejecución. Esto almacena en caché los diversos valores de retorno de una función durante una ejecución basada en varios argumentos. –

+0

@Mike Graham: Gracias, actualicé mi respuesta. – MAK

0

Por lo tanto, aquí hay una realmente buenos truco usando decoradores:

 
def memoize(f): 
    cache={}; 
    def result(*args): 
     if args not in cache: 
      cache[args]=f(*args); 
     return cache[args]; 
    return result; 

Con lo anterior, a continuación, puede utilizar:

 
@memoize 
def myfunc(x,y,z): 
    # Some really long running computation 

Cuando se invoca myfunc, sí que estará invocando la memoized versión de esto. Bastante limpio, ¿eh? Cada vez que desee volver a definir su función, basta con utilizar "@memoize" de nuevo, o escribir de forma explícita:

 
myfunc = memoize(new_definition_for_myfunc); 

Editar
No me di cuenta que quería caché entre varias ejecuciones.En ese caso, puede hacer lo siguiente:

 
import os; 
import os.path; 
import cPickle; 

class MemoizedFunction(object): 
    def __init__(self,f): 
     self.function=f; 
     self.filename=str(hash(f))+".cache"; 
     self.cache={}; 
     if os.path.exists(self.filename): 
      with open(filename,'rb') as file: 
       self.cache=cPickle.load(file); 

    def __call__(self,*args): 
     if args not in self.cache: 
      self.cache[args]=self.function(*args); 
     return self.cache[args]; 

    def __del__(self): 
     with open(self.filename,'wb') as file: 
       cPickle.dump(self.cache,file,cPickle.HIGHEST_PROTOCOL); 

def memoize(f): 
    return MemoizedFunction(f); 
+0

Bastante limpio, sí, pero no responde la pregunta. El punto fue que el código en el método cambió, no los parámetros de entrada a él. –

+0

Parece que OP quiere memorizar el valor de retorno de una función entre tiempos de ejecución. Esto almacena en caché los diversos valores de retorno de una función durante una ejecución basada en varios argumentos. –

+0

@Mike, está bien. No me di cuenta de que esto era entre ejecuciones del programa. –

0

La primera parte es la memorización y la serialización de su tabla de búsqueda. Eso debería ser lo suficientemente directo basado en alguna biblioteca de serialización de Python. La segunda parte es que desea eliminar su tabla de búsqueda serializada cuando cambia el código fuente. Tal vez esto está siendo pensado en alguna solución elegante. Presumiblemente, cuando cambias el código, lo registras en algún lugar. ¿Por qué no agregar un gancho a su rutina de registro que borra su tabla serializada? O si no se trata de datos de investigación y está en producción, hágalo parte de su proceso de liberación, si el número de revisión de su archivo (esta función se encuentra en su propio archivo) ha cambiado, su secuencia de comandos de liberación eliminará la tabla de búsqueda serializada.

Cuestiones relacionadas