Usar diseño por contrato en Python
design-by-contract (5)
El PEP que ha encontrado aún no ha sido aceptado, por lo que no existe una forma estándar o aceptada de hacerlo (sin embargo, ¡siempre puede implementar el PEP por su cuenta!). Sin embargo, hay algunos enfoques diferentes, como has encontrado.
Probablemente el más ligero sea simplemente usar decoradores de Python. Hay un conjunto de decoradores para las condiciones pre / post en la Biblioteca de Python Decorator que son bastante sencillos de usar. Aquí hay un ejemplo de esa página:
>>> def in_ge20(inval):
... assert inval >= 20, ''Input value < 20''
...
>>> def out_lt30(retval, inval):
... assert retval < 30, ''Return value >= 30''
...
>>> @precondition(in_ge20)
... @postcondition(out_lt30)
... def inc(value):
... return value + 1
...
>>> inc(5)
Traceback (most recent call last):
...
AssertionError: Input value < 20
Ahora, mencionas invariantes de clase. Estos son un poco más difíciles, pero la forma en que lo haría es definir un invocable para verificar el invariante, luego tener algo así como el decorador de post-condición comprobar que invariante al final de cada llamada al método. Como primer corte, probablemente puedas usar el decorador de post-condición tal como está.
Estoy buscando comenzar a utilizar DBC en una gran cantidad de proyectos basados en Python en el trabajo y me pregunto qué experiencias han tenido otros con él. Hasta ahora, mi investigación arrojó lo siguiente:
- http://www.python.org/dev/peps/pep-0316/ - PEP 316 que se supone debe estandarizar el diseño por contrato para Python que se ha aplazado. Este PEP sugiere usar docstrings.
- http://www.wayforward.net/pycontract/ - Contratos para Python. Esto parece ser un marco completo, pero no mantenido, que utiliza docstrings.
- http://www.nongnu.org/pydbc/ - PyDBC que implementa contratos usando metaclases. También no se mantuvo durante algunos años.
Mis preguntas son: ¿ha usado DBC con Python para el código de producción maduro? ¿Qué tan bien funcionó / valió la pena el esfuerzo? ¿Qué herramientas recomendarías?
En mi experiencia, vale la pena hacer un diseño por contrato, incluso sin soporte de idiomas. Para los métodos que no son aserciones anuladas, junto con las cadenas de documentos son suficientes tanto para precondiciones como para postcondiciones. Para los métodos que se anulan, dividimos el método en dos: un método público que verifica las condiciones previas y posteriores, y un método protegido que proporciona la implementación, y puede ser anulado por subclases. Aquí un ejemplo de este último:
class Math:
def square_root(self, number)
"""
Calculate the square-root of C{number}
@precondition: C{number >= 0}
@postcondition: C{abs(result * result - number) < 0.01}
"""
assert number >= 0
result = self._square_root(number)
assert abs(result * result - number) < 0.01
return result
def _square_root(self, number):
"""
Abstract method for implementing L{square_root()}
"""
raise NotImplementedError()
Obtuve la raíz cuadrada como un ejemplo general de diseño por contrato de un episodio de diseño por contrato en la radio de ingeniería de software ( http://www.se-radio.net/2007/03/episode-51-design-by-contract/ ). También mencionaron la necesidad de apoyo lingüístico porque las afirmaciones no fueron útiles para garantizar el principio de sustitución de Liskov, aunque mi ejemplo anterior apunta a demostrar lo contrario. También debería mencionar el modismo C ++ pimpl (implementación privada) como fuente de inspiración, aunque eso tiene un propósito completamente diferente.
En mi trabajo, recientemente refactoreé este tipo de verificación de contratos en una jerarquía de clases más grande (el contrato ya estaba documentado, pero no se probó sistemáticamente). Las pruebas unitarias existentes revelaron que los contratos fueron violados varias veces. Solo puedo concluir que esto debería haber sido hecho hace mucho tiempo, y que la cobertura de la prueba de unidad vale la pena aún más una vez que se aplica el diseño por contrato. Espero que cualquiera que pruebe esta combinación de técnicas haga las mismas observaciones.
Una mejor herramienta de soporte puede ofrecernos aún más poder en el futuro, eso lo agradezco.
Queríamos utilizar pre / post-condiciones en nuestro código de producción, pero descubrimos que todas las bibliotecas actuales de diseño por contrato carecían de mensajes informativos.
Por lo tanto, desarrollamos icontract . Los mensajes de error se generan automáticamente al volver a cruzar el código decompilado de la función y evaluar todos los valores implicados:
import icontract
>>> class B:
... def __init__(self) -> None:
... self.x = 7
...
... def y(self) -> int:
... return 2
...
... def __repr__(self) -> str:
... return "instance of B"
...
>>> class A:
... def __init__(self)->None:
... self.b = B()
...
... def __repr__(self) -> str:
... return "instance of A"
...
>>> SOME_GLOBAL_VAR = 13
>>> @icontract.pre(lambda a: a.b.x + a.b.y() > SOME_GLOBAL_VAR)
... def some_func(a: A) -> None:
... pass
...
>>> an_a = A()
>>> some_func(an_a)
Traceback (most recent call last):
...
icontract.ViolationError:
Precondition violated: (a.b.x + a.b.y()) > SOME_GLOBAL_VAR:
SOME_GLOBAL_VAR was 13
a was instance of A
a.b was instance of B
a.b.x was 7
a.b.y() was 2
Encontramos que la biblioteca es muy útil tanto en la producción (debido a los mensajes informativos) como durante el desarrollo (ya que permite detectar errores desde el principio).
Si bien no es exactamente el diseño por contrato , algunos marcos de prueba a favor del enfoque de prueba de propiedad son conceptualmente muy cercanos.
Las pruebas aleatorias para determinar si ciertas propiedades se mantienen en tiempo de ejecución permiten verificar fácilmente:
- invariantes
- dominios de valores de entrada y salida
- otras pre y poscondiciones
Para Python hay algunos marcos de prueba de estilo QuickCheck: