Pregunta

Estoy tratando de escribir un decorador de congelación para Python.

La idea es la siguiente:

(en respuesta a los dos comentarios)

Podría estar equivocado pero creo que hay dos usos principales de caso de prueba.

  • Uno es el desarrollo basado en pruebas: Idealmente, los desarrolladores están escribiendo casos antes de escribir el código. Por lo general, ayuda a definir la arquitectura porque esta disciplina obliga a definir las interfaces reales antes del desarrollo. Incluso se puede considerar que en algún caso la persona que despacha trabajo entre dev está escribiendo el caso de prueba y úselo para ilustrar eficientemente la especificación que tiene en mente. No tengo ninguna experiencia en el uso de casos de prueba como ese.

  • La segunda es la idea de que todos proyectan con un tamaño y varios programadores sufre de código roto. Algo que solía funcionar puede romperse por un cambio que parecía una refactorización inocente. Aunque es una buena arquitectura, la pareja suelta entre componentes puede ayuda a luchar contra este fenómeno; dormirás mejor por la noche si ha escrito algún caso de prueba para asegurarse que nada interrumpirá el comportamiento de su programa.

SIN EMBARGO, Nadie puede negar la sobrecarga de escribir casos de prueba. En el primer caso, uno puede argumentar que el caso de prueba es realmente orientador desarrollo y, por lo tanto, no debe considerarse una sobrecarga.

Hablando francamente, soy un programador bastante joven y si fuera usted, mi palabra sobre este tema no es realmente valiosa ... De todos modos, creo que la empresa / proyectos mosts no funcionan así, y que las pruebas unitarias se usan principalmente en el segundo caso ...

En otras palabras, en lugar de garantizar que el programa es funcionando correctamente, tiene como objetivo comprobar que lo hará trabajará igual en el futuro.

Estas necesidades pueden satisfacerse sin el costo de escribir pruebas, utilizando este decorador de congelación.

Digamos que tienes una función

def pow(n,k):
    if n == 0:  return 1
    else:       return n * pow(n,k-1)

Es perfectamente agradable y desea reescribirlo como una versión optimizada. Es parte de un gran proyecto. Quieres que devuelva el mismo resultado por unos pocos valores. En lugar de pasar por el dolor de los casos de prueba, uno podría usar algunos tipo de decorador de congelación.

Algo tal que la primera vez que se ejecuta el decorador, el decorador ejecuta la función con los argumentos definidos (debajo de 0 y 7) y guarda el resultado en un mapa (f - > args - > result)

@freeze(2,0)
@freeze(1,3)
@freeze(3,5)
@freeze(0,0)
def pow(n,k):
    if n == 0:  return 1
    else:       return n * pow(n,k-1)

La próxima vez que se ejecute el programa, el decorador cargará este mapa y comprobará que el resultado de esta función para estos argumentos no ha cambiado.

Ya escribí rápidamente el decorador (ver abajo), pero lastimé algunos problemas sobre que necesito tu consejo ...

from __future__ import with_statement
from collections import defaultdict
from types import GeneratorType
import cPickle

def __id_from_function(f):
    return ".".join([f.__module__, f.__name__])

def generator_firsts(g, N=100):
    try:
        if N==0:
            return []
        else:
            return  [g.next()] + generator_firsts(g, N-1)
    except StopIteration :
        return []

def __post_process(v):
    specialized_postprocess = [
        (GeneratorType, generator_firsts),
        (Exception,     str),
    ]
    try:
        val_mro = v.__class__.mro()
        for ( ancestor, specialized ) in specialized_postprocess:
            if ancestor in val_mro:
                return specialized(v)
        raise ""
    except:
        print "Cannot accept this as a value"
        return None

def __eval_function(f):
    def aux(args, kargs):
        try:
            return ( True, __post_process( f(*args, **kargs) ) )
        except Exception, e:
            return ( False, __post_process(e) )
    return aux

def __compare_behavior(f, past_records):
    for (args, kargs, result) in past_records:
        assert __eval_function(f)(args,kargs) == result

def __record_behavior(f, past_records, args, kargs):
    registered_args = [ (a, k) for (a, k, r) in past_records ]
    if (args, kargs) not  in registered_args:
        res = __eval_function(f)(args, kargs)
        past_records.append( (args, kargs, res) )

def __open_frz():
    try:
        with open(".frz", "r") as __open_frz:
            return cPickle.load(__open_frz)
    except:
        return defaultdict(list)

def __save_frz(past_records):
    with open(".frz", "w") as __open_frz:
        return cPickle.dump(past_records, __open_frz)


def freeze_behavior(*args, **kvargs):
    def freeze_decorator(f):
        past_records = __open_frz()
        f_id = __id_from_function(f)
        f_past_records = past_records[f_id]
        __compare_behavior(f, f_past_records)
        __record_behavior(f, f_past_records, args, kvargs)
        __save_frz(past_records)
        return f
    return freeze_decorator
  • El volcado y la comparación de resultados no es trivial para todo tipo. En este momento estoy pensando en usar una función (aquí la llamo postproceso) para resolver este problema. Básicamente, en lugar de almacenar res, almaceno postprocess (res) y comparo postprocess (res1) == postprocess (res2), en lugar de comparar res1 res2. Es importante dejar que el usuario sobrecargue la función predefinida de postproceso. Mi primera pregunta es: ¿Conoce una forma de verificar si un objeto es volcado o no?

  • Definir una tecla para la función decorada es un dolor. En los siguientes fragmentos Estoy usando el módulo de funciones y su nombre. ** ¿Se te ocurre una forma más inteligente de hacerlo? **

  • Los fragmentos a continuación funcionan, pero abren y cierran el archivo durante la prueba y la grabación. Este es solo un prototipo estúpido ... pero ¿conoces una buena manera de abrir el archivo, procesar el decorador para todas las funciones, cerrar el archivo ...

  • Tengo la intención de agregar algunas funcionalidades a esto. Por ejemplo, agregue la posibilidad de definir un iterable para examinar un conjunto de argumentos, registrar argumentos de uso real, etc. ¿Por qué esperarías de un decorador así?

  • En general, ¿utilizaría una función así, knowi

¿Fue útil?

Solución

" En general, ¿utilizaría dicha función, conociendo su limitación ...? "

Hablando francamente, nunca.

No hay circunstancias bajo las cuales yo "congelaría" resultados de una función de esta manera.

El caso de uso parece estar basado en dos ideas equivocadas: (1) que la prueba de la unidad es difícil o compleja o costosa; y (2) podría ser más simple escribir el código, "congelar" los resultados y de alguna manera usar los resultados congelados para refactorizar. Esto no es útil. De hecho, la posibilidad muy real de congelar respuestas incorrectas hace que sea una mala idea.

Primero, en "consistencia vs. corrección". Esto es más fácil de preservar con un mapeo simple que con un conjunto complejo de decoradores.

Haga esto en lugar de escribir un decorador congelado.

print "frozen_f=", dict( (i,f(i)) for i in range(100) )

El objeto de diccionario que se crea funcionará perfectamente como un conjunto de resultados congelado. Sin decorador. No hay complejidad de la que hablar.

Segundo, en " pruebas unitarias " ;.

El punto de una prueba unitaria es no para "congelar" Algunos resultados al azar. El objetivo de una prueba unitaria es comparar los resultados reales con los resultados desarrollados en otra (forma más simple, más obvia y de bajo rendimiento). Por lo general, las pruebas unitarias comparan resultados desarrollados a mano. Otras veces, las pruebas unitarias utilizan algoritmos obvios pero horriblemente lentos para producir algunos resultados clave.

El punto de tener datos de prueba no es que esté "congelado" resultado. El punto de tener datos de prueba es que es un resultado independiente. Hecho de manera diferente, a veces por diferentes personas, eso confirma que la función funciona.

Lo siento. Esto me parece una mala idea; parece que subvierte la intención de las pruebas unitarias.


" SIN EMBARGO, nadie puede negar la sobrecarga de escribir casos de prueba "

En realidad, mucha gente negaría la "sobrecarga". No es "gastos generales" en el sentido de pérdida de tiempo y esfuerzo. Para algunos de nosotros, las pruebas unitarias son esenciales. Sin ellos, el código puede funcionar, pero solo por accidente. Con ellos, tenemos amplia evidencia de que realmente funciona; y los casos específicos para los que funciona.

Otros consejos

¿Está buscando implementar invariantes o publicar condiciones?

Debe especificar el resultado explícitamente, esto eliminará la mayoría de sus problemas.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top