Cuando creas código, debes poder confiar en algunas cosas, como el sistema operativo o el intérprete de Python. Si no, sería casi imposible encontrar un error. Por suerte casi todo lo que usamos es lo suficientemente maduro para tener la certeza de que si algo no funciona la causa está en nuestro código.
¿Qué pasa con las librerías de terceros? También tenemos que confiar en ellas y para ello hay que comprobar los parámetros de entrada. Esta entrada te propone que hagamos juntos una reflexión sobre este tema.
Verificando parámetros
Una librería es un fragmento de código muy delicado. No está pensada para ser utilizada por un usuario final a través de una interfaz que limite y controle lo que el usuario pueda hacer. Una librería está pensada para ser parte de otro código y para permitir que sean programadores o software craftmen quienes la utilicen a veces de manera muy ingeniosa.
Para que una librería tenga éxito y los desarrolladores la utilicen es fundamental que sea de fiar. Una librería podrá hacer su trabajo mejor o peor, incluso puede no ser capaz de trabajar en determinadas circunstancias, pero si obtenemos un error de nuestro código siempre debe estar claro si el error es interno de la propia librería y hemos encontrado un bug o nos topamos con una limitación documentada, si el error es de nuestro código que usa mal la librería o si el error es de nuestro código y no tiene nada que ver con la librería.
Para conseguir esto es fundamental tratar los parámetros que reciben los métodos o funciones de la librería adecuadamente. Tomemos como ejemplo una sencilla librerías de funciones matemáticas básicas: potencia, exponencial, divisores, mínimo común divisor, máximo común múltiplo, etc.
Algunas de estas funciones no aceptan números negativos, ¿tenemos que comprobar todos los métodos? Sí.
Si un parámetro negativo producirá una excepción más adelante, ¿para qué esperar?. Una buena alternativa es comprobar el parámetro justo al comienzo de la llama al método o función y lanzar una excepción. Por ejemplo, imagina esta implementación básica de la función de Fibonacci.
def fib(n): if n == 0: return 0 elif n == 1: return 1 else: return fib(n-1) + fib(n-2)
¿Cuál es la diferencia entre verificar el valor del parámetro o dejar que el código falle dónde tenga que fallar? Compruébalo tú mismo viendo cuál de los dos siguientes errores es más claro.
Sin comprobar valores de entrada.
Comprobando valores de entrada.
Si comprobamos todos los parámetros de entrada, estaremos escribiendo una librería sólida que ayuda a sus usuarios a escribir buen código. Pero también podemos estar repitiendo muchas veces el código de comprobación y repetir código siempre es malo.
Por suerte tenemos varias alternativas. Además, resolver este problema nos da una indicación de nuestro nivel de programación. Algunas opciones son:
- Creamos un método interno auxiliar que compruebe los parámetros. Todos nuestros métodos llamarían a dicho método (así está implementada las clases de java.collection, por ejemplo).
- Creamos un decorador que compruebe los parámetros (los decoradores no son tan difíciles como parece).
- Utilizamos alguna librería de comparaciones, como sure o expects.
Si se te ocurren más soluciones ponlas como comentario de este artículo. No te preocupes si crees que no son mejores que las que ya hemos visto, porque siempre nos servirá para aprender un poco más.
Unidades con magnitudes
Otro tema interesante es cómo trabajar con magnitudes que tienen una unidad asociada. Por ejemplo, pensemos en la temperatura. Si nos dan estas temperaturas: 10, 283 y 50, ya podemos cruzar los dedos para que el termómetro se haya estropeado.
Pero si nos dicen que la temperatura es 10 grados centígrados, 283 grados kelvin y 50 grados Fahrenheit descubrimos que, en verdad, estamos hablando de la misma temperatura y que mejor vamos abrigados o pasaremos frio.
A Python y a nuestras librerías le pasa lo mismo, a veces necesita las unidades porque un número no les dice nada. Pero Python no tiene ningún tipo de dato nativo para empresar magnitudes y unidades. Lo mejor es crearlo nosotros mismos con una clase. ¿Cómo podría ser una clase temperara (llamada temp para abreviar)?
- Podríamos crear métodos estáticos, por ejemplo tem.kelvin(50) o temp.celcius(-4) que comprobaran que una temperatura es válida en el rango de sus unidades.
- Podríamos crear métodos de conversión para obtener la temperatura en la magnitud que deseemos, por ejemplo si creamos temp.kelvin(283) y vemos el valor de temp. celcius () obtenemos un correcto 10.
- Podríamos sobrecargar los operadores para comparar y operar con temperaturas más intuitivamente.
- Y podríamos buscar si alguien ya lo ha hecho por nosotros.
Nos evitaríamos, por ejemplo el problema de mezclar distintas magnitudes, ya que la clase verificaría por nosotros que las operaciones se realicen siempre con la misma unidad.
Bola extra
Existe una técnica de diseño 7 programación conocida como diseño por contratos (design by contract) en las que, para cada operación se especifican sus precondiciones, post-condiciones e invariantes y luego se implementan en el código.
Esta técnica alcanzó popularidad a finales de los 90 por el desastre el cohete Ariane 5, el cual se podría haber evitado especificando los contratos de las operaciones.
Si os manejáis por el mundo Java, podéis ver esta técnica en aplicación por ejemplo en java.collection, aunque luego no implementéis todas las precondiciones, poscondiciones e invariante sí es una buena idea pensar en los contratos de vuestras librerías. Además, también sirve de ayuda para diseñar casos de prueba.
¡Muy interesante el artículo Javier! Ya he visto que no olvidas una 🙂 Me ha dado muchas ideas interesantes como siempre.
Lo del diseño por contrato me ha encantado, creo que voy a empezar a tenerlo más en cuenta de cara al futuro.
Para lo de las unidades, estoy probando con una filosofía nueva a ver qué te parece: para la biblioteca poliastro que estoy reescribiendo estoy usando ahora el paquete astropy.units y dividiendo la API en dos capas. La capa superior funciona más orientada a objetos y requiere que introduzcas cantidades con unidades, de modo que las rutinas son «seguras» en ese sentido. La capa inferior es más procedural y funciona tanto con cantidades con unidades como con valores numéricos, y en este último caso los resultados son correctos siempre que el usuario sea consistente. La ventaja es que usando valores numéricos los algoritmos son sensiblemente más rápidos, así que si en un momento dado se requiere velocidad pueden usarse las funciones «inseguras». O incluso puedo implementarlo yo de tal forma que las funciones seguras quiten las unidades a los argumentos de entrada y usen las inseguras, pero aún no lo he decidido.
Muchísimas gracias por seguir colaborando con nosotros, siempre es un placer leerte 🙂
Juanlu gracias a ti por llevar el blog y estar siempre investigando cosas nuevas.
La opción de que la parte segura valide y luego llame a la parte procedural optimizada me parece muy interesante. Además si es de código abierto quien quiera ir directamente a la parte procedural no tiene más qué ver que módulos y funciones se usan y llamarla directamente (bajo su propio riesgo) 😉
Primero. Las librerías tienen éxito en función de su utilidad. Algunas veces, no siempre, una librería es útil y funciona. Si no es el caso, pues uno tiene que modificarla o reescribirla. Fool me once, shame on you. Fool me twice, shame on me. Lo del … o no la hagas, suena mal a quienes hemos publicado liberías, ya sean buenas o malas, porque hemos trabajado gratis. Digo yo que haré lo que me de la gana.
Lo segundo. Python es un lenguaje pensado para hacer cosas rápidas y razonablemente leíbles. Los intentos de introducir interfaces han sido un desastre sin precedentes (Zope 3 mató Zope). Si quiero fiarme de verdad, no usaré Python. No me fío del intérprete, y tengo motivos para ello. Hay lenguajes mejores para eso.
Tercero. Un sistema es tan robusto como la mente de quien lo diseña. Muchísimo antes que hablar sobre qué hacer con parámetros o argumentos, hay que pensar qué está haciendo uno de verdad cuando clona un repositorio de Github. Pensar que algo es seguro porque tiene “duct typing” (no es una errata) es no haber entendido nada.
Ya para terminar. Lo del Arianne 5 fue por fiarse de un blob binario tras un cambio de arquitectura. Ahí no hay interfaz que valga, porque cogieron un driver para µc de 16 bits y lo enchufaron a un procesador de 32 bits. Esto no tiene nada que ver con lo de fiarse de los interfaces, sino que va de lógica francesa, y de crucificar a gente que se lo merece.
Hala, ya me he desahogado.
Hola Guillem, gracias por el comentario y espero que te hayas quedado desahogado. No me corresponde a mí decir si lo de «o no lo hagas» te lo puedes tomar como algo personal, pero si me permites la sugerencia tal vez se trate de un inocuo truco estilístico, porque si te das cuenta lo que viene después son consejos para principiantes a un nivel muy didáctico. Si es en realidad una crítica a la gente que escribe open source y que obviamente hace lo que le da la gana entonces efectivamente no he entendido nada. Yo también he escrito librerías gratis y muy malas además, así que aprecio cualquier consejo.
En segundo lugar. Zope 3 se publicó en noviembre de 2004, y a falta de unos días la versión estable de Python era 2.3. Eso era antes de la unificación de enteros, antes de los generadores, de los decoradores, del bloque
with
y de más cosas, y traerlo a colación no me parece demasiado relevante. El packaging ha sido un fracaso durante una década y ahora es decente, y el soporte a I/O asíncrona también y ahora ha salido tulip. Python va a seguir evolucionando y seguirá siendo por prueba-error.Tercero. Una cosa es que a uno le guste el duct typing [sic] y otra que le parezca seguro. Si no me iría a Montegancedo a coger un libro de C++.
Y cuarto. Gracias por puntualizar en qué consistió el fallo del Ariane 5, pero estarás de acuerdo conmigo en que los fallos de programación se pueden evitar y prevenir con multitud de técnicas, y que en todo caso no creo que se pretendiese transmitir al lector que con un decorador en Python se habría evitado esa catástrofe.
Un saludo.
Hola Guillem.
Me uno al agradecimiento por el tiempo que nos dedicas. Además tus comentarios me han servido para leer tu blog y conocer tu trabajo, que tiene muy buna pinta.
Como te comenta Juanlu en su respuesta (¡¡¡gracias!!!) el título simplemente es algo llamativo, no hay que tomárselo al pie de la letra. No quería molestar a nadie y te pido disculpas si ha sido así.
Un contrato no es solo una interfaz, por ejemplo, del lenguaje Java. De hecho una interfaz de Java o los métodos virtuales de C++, no pueden capturar la semántica del contrato, por eso o bien existen lenguajes con instrucciones para ello, como Effiel, o bien se utilizan lenguajes de especificación, como Java Modeling Language, o bien se usan otras herramientas como documentar bien (como pasa en el API de java.collection) o utilizar pruebas para definir esos contratos (como la típica e comprobar que len(x) se incrementa en uno cuando haces append sobre x).
Aquí tienes un artículo que defiende que, con diseño por contratos se habría evitado el error del Ariadne. No digo que sea cierto o no, o que no haya más causas, pero sí hay referencias importantes que defienden esta idea: http://archive.eiffel.com/doc/manuals/technology/contract/ariane/
Por lo demás, en general, tus comentarios me parecen opiniones interesantes y que animan a pensar, pero no estoy nada de acuerdo. Cada uno tiene sus opiniones y es muy interesante que haya cuanto más variedad mejor.
Disculpad por poner tantos ejemplos de Java, se me nota de dónde vengo 😉
PD: Gracias a Juanlu por la brevísima historia de Python, parece un tema interesante 😉
¡¡Gracias por tu trabajo Juanlu!! Sigue así,gente como tú hace falta para divulgar y aprender Python.Sigue en esta línea, estás haciendo un trabajo estupendo que mucha gente valoramos muchísmo.No solo los artículos y trabajos,sino también el tono, el respeto y el entusiasmo que pones. Un abrazo.
Perdón Javier,no me di cuenta de que el artículo era tuyo.Me ha parecido muy interesante y por supuesto te animo a seguir colaborando con Juanlu para que Pybonacci sea cada vez mejor.También me gustan mucho los comentarios de nivel,pero no olvidéis que el buen rollo es fundamental.En el fondo todos queremos aprender de gente que sabe mucho más y no entrar en otras dinámicas que no ayudan a la divulgación.Es genial tener sitios de nivel en castellano sobre temas científicos o técnicos y es de agradecer el esfuerzo realizado.Muchas gracias y…ánimo.Saludos.
Gracias Alberto. Aunque Juanlu no haya escrito el artículo creo que todo lo que le dices es merecidísimo 😉
A ver si rediseñamos el blog y ponemos el nombre del autor un poco más visible 🙂 ¡Gracias por los comentarios! Y el mérito todo para Javier 😉
Una posible opción para trabajar con cantidades y unidades: Quantity Pattern http://martinfowler.com/eaaDev/quantity.html
¡Gracias por la sugerencia! Ahora mismo estoy usando astropy.units y la verdad es que estoy encantadísimo. Además gracias a eso están introduciendo varias mejoras en los propios arrays de NumPy. Definitivamente lo recomiendo 🙂
Pingback: Python científico en paralelo (cursos y tutoriales rápidos para principiantes) | CAChemE