2.7. Estilo de programación
Objetivo de la sección
El objetivo de esta sección es introducir algunos elementos que no son obligatorios al programar pero que hacen el código mucho más comprensible y fácil de mantener.
En las secciones anteriores hemos empezado a estudiar la sintaxis de Python, es decir las reglas que define el lenguaje y que deben respetarse en todos los programas que se escriban con él: si las reglas sintácticas no se cumplen, el intérprete de Python no podrá entender lo que quiere decir el programa y no podrá ejecutarlo. Sin embargo, la sintaxis no lo es todo en un lenguaje de programación. También es importante pensar en el estilo.
Si bien las reglas de Python parecen muy estrictas, hay infinidad de formas diferentes en las que se puede escribir el mismo programa. Esto incluye desde cambios menores, como incluir líneas en blanco o comentarios adicionales, hasta cambiar la descomposición en funciones, pasando por cambios en los nombres de las variables y de las funciones. Es decir que dos personas que escriban el mismo programa Python pueden terminar escribiendo programas muy diferentes sólo por utilizar un estilo diferente.
El problema con esta gran libertad es que las decisiones que tomemos con respecto al estilo del código que escribamos hoy posiblemente nos van a acompañar durante mucho tiempo. La mayoría de los programas se escriben en un tiempo relativamente corto (días o semanas) y luego se utilizan, se corrigen y se actualizan durante meses o años. Si hoy tomamos decisiones que parecen fáciles (como utilizar nombres de variables cortísimos o no descomponer una función muy complicada), es posible que más adelante sea más difícil arreglar un problema porque no podremos entender con facilidad nuestro propio código.
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
—Martin Fowler
Para ilustrar este punto, revisemos el siguiente programa basado en las funciones que usamos en la sección anterior:
>>def area_cuadrado(lado: int)-> int:
>> """ Calcula el área de un cuadrado dada la medida de su lado
>> Parámetros:
>> lado (int): La medida del cuadrado
>> Retorno:
>> (int): El valor del área del cuadrado. Es siempre un número entero.
>> """
>> return lado * lado
>>def area_triangulo(base: int, altura: int)-> float:
>> """ Calcula el área de un triángulo.
>> Parámetros:
>> base (int): La medida de la base del triángulo.
>> altura (int): La medida de la altura del triángulo.
>> Retorno:
>> (float): El valor del área del triángulo. Es siempre un número decimal.
>> """
>> return (base * altura) / 2
>>def area_casa(frente: int, techo: int)-> float:
>> """ Calcula el área del dibujo de una casa que se forma con un cuadrado
>> y un triángulo encima (el techo).
>> El frente de la casa será igual al lado del cuadrado y a la base del triángulo.
>> La altura del techo será la altura del triángulo.
>> Parámetros:
>> frente (int): La medida del frente de la casa.
>> techo (int): La medida de la altura del techo de la casa.
>> Retorno:
>> (float): El valor del área del dibujo de la casa.
>> """
>> cuadrado = area_cuadrado(frente)
>> triangulo = area_triangulo(frente, techo)
>> return cuadrado + triangulo
>>print(area_casa(10, 5))
Ahora revisemos un segundo programa:
>>def f(b, c):
>> v = (b * b) + (b * c)/2
>> return v
>>print(f(10, 5))
Aunque a primera vista no es evidente, los dos programas son
equivalentes en el sentido de que al ejecutarlos el resultado será el
mismo: imprimirán el valor 125.0
en la consola. Evidentemente el segundo programa es mucho más corto que
el primero, pero esto no necesariamente es una ventaja. En este caso es
difícil entender que la función f
sirve para calcular el área del dibujo de la casa y requiere como parámetro las medidas del frente y del techo, en ese orden.
Podemos decir que las diferencias entre los dos programas se reducen a los siguientes 5 aspectos:
Utilizar nombres claros para las variables, las funciones y los parámetros.
Documentar el objetivo de cada función
Descomponer las funciones para que cumplan objetivos precisos
Complejidad de las instrucciones
Indicar los tipos de los parámetros y retornos de las funciones
A continuación revisaremos cada uno de estos puntos con un poco más de detalle.
2.7.1. Nombramiento de variables y funciones
Uno de los factores que más incide en la facilidad para comprender un programa es la selección de nombres para variables y funciones: si el nombre que utilizamos para una variable o una función es bueno, no tendremos que pensar mucho para recordar qué rol tiene dentro del programa y podremos concentrarnos en los aspectos importantes.
En el ejemplo que presentamos antes podemos ver esto claramente:
>>def area_triangulo(base: int, altura: int)-> float:
>> return (base * altura) / 2
>>def g(b, c):
>> return (b * c)/2
Las dos funciones anteriores hacen los mismos cálculos, pero la primera es claramente más explícita: no tenemos que hacer un gran esfuerzo para descubrir cuál es su objetivo.
Use buenos nombres de variables:
Para las variables, utilice nombres que indiquen con claridad qué es lo que va a guardar dentro de ellas. Evite nombres muy cortos a menos que no haya ninguna ambigüedad posible.
Use buenos nombres de funciones:
Para las funciones, utilice nombres que indiquen qué es lo que hará la función.
Incluya verbos en los nombres de funciones (ej. guardar_resultado
), a menos que se pueda sobreentender con facilidad (ej. calcular_area_triangulo
vs. area_triangulo
).
2.7.1.1. Estándares
Más allá de los nombres y de lo que significan, en cada lenguaje también hay estándares para el uso de mayúsculas y minúsculas y la separación de palabras en los nombres de variables y funciones. Estos estándares pueden parecer arbitrarios (¡y lo son!) pero es importante respetarlos porque seguirlos consistentemente hace mucho más sencilla la lectura del código.
En Python, las reglas más importantes en este sentido son las siguientes:
Usar snake_case. Esto significa que las palabras de un identificador deberían separarse usando el caracter
'_'
. Por ejemplo, en Python se prefiere usarcalcular_area_triangulo
mientras que en Java se usaríacalcularAreaTriangulo
.Usar minúsculas para los identificadores. Tanto funciones como variables y parámetros deberían nombrarse usando minúsculas.
Usar Mayúscula Inicial para los nombres de clases 1.
Usar MAYÚSCULAS SOSTENIDAS para las constantes. Aunque estrictamente hablando en Python no existe el concepto de constante, se suelen usar mayúsculas sostenidas para indicar que el valor de una variable no debería cambiar su valor. Por ejemplo,
ROJO
oIVA
. Desafortunadamente una de las constantes más útiles,math.pi
, no sigue este estándar.
Use el alfabeto inglés:
ñ
y las vocales acentuadas, es recomendable evitarlo para evitar
problemas de codificación. Esto es especialmente importante si se va a
usar el mismo código en máquinas Windows, Linux y Mac
2.7.2. Documentación de funciones
Un segundo aspecto para facilitar el uso de nuestro código es documentar las funciones con un comentario que le sirva a potenciales usuarios o a nosotros mismos. De esta forma no será necesario estudiar con detenimiento el cuerpo de la función para saber qué hace.
Para cada función usualmente queremos saber 4 cosas:
cuál es su objetivo
cómo se debe usar
qué pasará cuando se use
cómo deben usarse y qué representan los parámetros
En Python el comentario con múltiples líneas que se encuentre justo
después de la signatura de una función es considerado la documentación
de la función. Esto usualmente se conoce como el docstring
de una función y tiene una característica muy importante: cuando busquemos ayuda sobre una función, usando la función nativa help
, recibiremos el docstring
. Veamos un ejemplo en el que primero definiremos una nueva función y especificaremos su docstring
.
>>def area_triangulo(base: int, altura: int)-> float:
>> """ Calcula el área de un triángulo a partir de su base y su altura.
>> Tanto la base como la altura deben ser números enteros.
>> El resultado es un número decimal aunque los parámetros sean enteros.
>> """
>> return (base * altura) / 2
Si después de definir nuestra función invocamos la función nativa help
usando nuestra función como parámetro, obtendremos la documentación que especificamos.
>>> help(area_triangulo)
>>>Help on function area_triangulo in module __main__:
>>>area_triangulo(base: int, altura: int) -> float
>>> Calcula el área de un triángulo a partir de su base y su altura.
>>> Tanto la base como la altura deben ser números enteros.
>>> El resultado es un número decimal aunque los parámetros sean enteros.
Ahora bien, a diferencia de otros lenguajes Python no especifica cómo deben describirse los detalles de una función: sólo nos da el espacio para que escribamos la documentación y nos da total libertad para que nosotros decidamos qué aspectos queremos documentar. Es nuestra responsabilidad decidir qué incluir y asegurarnos de que la documentación sea suficiente para que alguien más pueda usar nuestra función. También es nuestra responsabilidad definir cómo vamos a organizar la información para que esté organizada y sea fácil de encontrar y utilizar.
Toda esta libertad que da el lenguaje ha llevado a que existan varios estándares para documentar las funciones sin que ninguno sea claramente superior a los otros. Aunque le recomendamos que más adelante escoja uno de los estándares, por ahora le recomendamos utilizar la versión simplificada que se ilustra en el siguiente ejemplo:
>>""" Calcula el área de un triángulo a partir de su base y su altura.
>>Parámetros:
>> base (int): La medida de la base del triángulo.
>> Debe ser un número estrictamente positivo (mayor o igual a 1).
>> altura (int): La medida de la altura del triángulo.
>> Debe ser un número estrictamente positivo (mayor o igual a 1).
>> Retorno:
>> (float): El valor del área del triángulo. Es siempre un número decimal.
>> """
Esta documentación incluye los siguientes elementos:
Descripción de la función. Acá explicamos cuál es el objetivo de la función para que sea fácil saber si es la función que necesitamos usar. Además, si la función es muy complicada, explicamos qué es lo que hace la función por dentro. Esta descripción puede ocupar múltiples líneas: le recomendamos que no use líneas muy largas y que intente que el texto quede bien alineado a la izquierda.
Parámetros. Si la función tiene parámetros, especificamos el nombre y el tipo de cada uno, seguidos de una descripción. La idea es que quien vaya a utilizar la función se entere de qué representa el parámetro y de todas las reglas que deberían aplicarse.
Retorno. Acá explicamos qué es lo que retorna la función.
Para una función tan sencilla como la del ejemplo, puede parecer que esta descripción tan grande es exagerada. Pronto estaremos trabajando con funciones mucho más complicadas en las que será muy importante que escribamos una documentación muy completa para que no nos confundamos nosotros mismos o confundamos a las personas con las que estemos trabajando.
Documente sus funciones:
Documente siempre sus funciones utilizando un formato consistente que incluya una descripción general y la explicación detallada de los parámetros y el retorno.
2.7.2.1. Ejercicios
Use la función
help
para consultar la documentación de algunas de las funciones nativas que ya ha estudiado.Escriba la documentación de alguna función que haya desarrollado en un ejercicio previo. Revise en el intérprete de Python que pueda leer la documentación de la función usando la función
help
.En PPI para dar respuesta a estos ejercicios lo vamos a hacer con ejercicios planteados por el profesor Argentino Alfredo del conocido recurso educativo gratuito aprende con Alf.
Ejercicio 1
Escribir una función que muestre por pantalla el saludo ¡Hola amiga! cada vez que se la invoque.Solución:
El código para dar solución al ejercicio 1 de la sección de funciones del profesor Alfredo es la siguiente:
>>print('Inicio de ejercicio 1')
>># Solucion de PPI (python para ingenieros)
>>def saludo()->str:
>> """Esta funcion muestra por pantalla el saludo ¡Hola amiga! sin utilizar ningun parametro y >>la salida es tipo str."""
>> return print("¡Hola amiga!")# LLamamos o invocamos la funcion:
>>saludo()
>>print('Fin de ejercicio 1')#Se obtiene la respuesta de la consola como:
>>>Inicio de ejercicio 1 >>>¡Hola amiga! >>>Fin de ejercicio 1
#Consultemos ahora el docstring de la funcion saludo():
>>help(saludo)
>>>Help on function saludo in module __main__: >>>saludo() -> str >>> Esta funcion muestra por pantalla el saludo ¡Hola amiga! sin utilizar
>>>ningun parametro y la salida es tipo str.
2.7.3. Descomposición de funciones
Aunque no se puede generalizar, en lo posible deberíamos tener funciones sencillas que se compongan poco a poco para formar funciones más complicadas. Esto es preferible a tener funciones extremadamente complicadas que se tengan que leer con muchísima atención: al leer el código de una función debería ser claro cuál es su objetivo principal y cómo lo está logrando.
Para lograr una buena descomposición es necesario primero hacer abstracción de las funciones, separando la signatura de la implementación. Es decir, debemos pensar en qué se quiere lograr con una función independientemente de cómo se vaya a implementar. El proceso se debe repetir identificando funciones cada vez más sencillas que sirvan para explicar cómo se resuelven las funciones más grandes, pero sin entrar en detalles, hasta que lleguemos a funciones triviales. En una sección posterior estudiaremos en mucho más detalle este proceso que se conoce como refinamiento a pasos.
Simplifique sus funciones
Intente tener funciones que tengan un único objetivo y que sean fáciles de explicar. Si usted descubre que el objetivo o la implementación de una función son muy complicados de explicar, posiblemente sea una señal de que debe descomponerla en funciones más pequeñas.
El siguiente motivo por el cual tiene sentido descomponer las funciones es para evitar la repetición de código. En general, tener código repetido es mala idea porque aumenta la posibilidad de tener errores y porque, en caso de querer corregir un error, será necesario hacerlo en muchos lugares.
Use funciones para evitar repeticiones
Si está repitiendo el mismo código en varios lugares, considere construir una función que encapsule esa funcionalidad y que pueda llamar en todos los lugares donde lo requiera2.7.4. Complejidad de las instrucciones
Una razón por la cual muchas veces el código es mucho más difícil de leer y entender de lo necesario es porque se hacen muchas acciones dentro de la misma instrucción. A manera de ejemplo a continuación presentamos dos funciones equivalentes que calculan el área de un polígono regular a partir de la longitud de un lado y de la cantidad de lados:
>>import math
>>def area_poligono(lado: float, num_lados: int) -> float:
>> return (num_lados * lado**2) / (4 * math.tan(math.pi / num_lados))>>import math
>>def area_poligono2(lado: float, num_lados: int) -> float:
>> angulo_interno_radianes = math.pi / num_lados
>> numerador = num_lados * lado**2
>> denominador = 4 * math.tan(angulo_interno_radianes)
>> return numerador / denominadorAunque el primer ejemplo no es extremadamente complicado, la única instrucción que tiene es mucho más complicada que cualquiera de las instrucciones del segundo ejemplo. Esto significa que el segundo ejemplo será más fácil de leer y probablemente fue más fácil de construir que el primero.
Simplifique las instrucciones:
- Escriba instrucciones que sean lo más sencillas posibles.
- Idealmente, cada línea de código debería hacer una sola cosa
2.7.5. Tipado de funciones y parámetros
Si usted utiliza otros libros o si consulta en Internet, es muy posible que se encuentre con definiciones de funciones en las que no aparecen los tipos de los parámetros ni el tipo de los resultados. Esto se debe a que en Python el uso de estos elementos es opcional. De hecho, el nombre específico de estos elementos es type-hints y las herramientas (IDE, intérprete, compilador, etc.) los utilizan sólo como sugerencias.
En este libro vamos a usar type-hints en la definición de todas las funciones y esperamos que usted haga uso de ellos también. Por una parte, esto le facilitará aprender a usar otros lenguajes de programación como C, C++, Java, o TypeScript. Por otro lado, razonar sobre los tipos de datos debería ayudarlo a estructurar mejor sus programas, especialmente mientras adquiere una cierta destreza programando.
Utilice type-hints
Utilice los type-hints para todos los parámetros y los retornos de las funciones. No sólo harán que su código sea más legible y fácil de usar, sino que además lo prepararán a usted para utilizar otros lenguajes.
2.7.6. Comentarios
Por último, hay un aspecto adicional que es muy sencillo pero tiende a mejorar la calidad del código: introducir comentarios dentro de las instrucciones. En los ejemplos que introducimos al principio de la sección eso no se estaba haciendo porque las funciones utilizadas eran muy sencillas, pero en funciones como las que estudiaremos a partir de la próxima sección esto será mucho más importante.
En general deberían incluirse comentarios dentro del código para explicar el funcionamiento de bloques de código que sean particularmente complicados. No existe ningún estándar sobre esos comentarios, pero a continuación le damos algunas recomendaciones:
No exagere con los comentarios. Así como la falta de comentarios es grave, el exceso de comentarios puede terminar en código muy difícil de leer.
Escriba comentarios que expliquen lo que hacen fragmentos significativos de código y/o su justificación, en lugar de hacer una traducción de Python a español de las instrucciones realizadas.
Identifique las instrucciones particularmente complicadas y documéntelas.
En funciones medianas o largas que no pueda o no quiera descomponer, enumere las grandes etapas usando comentarios.
2.7.7. Ejercicios
1. Revise detenidamente la siguiente función para descubrir su objetivo. Reescribirlo aplicando las recomendaciones que se estudiaron en esta sección.
>>def vc(r, a) :
>> b = 3.14159 * (r**2)
>> return round(b * a,2)Solución al item 1. Fuente:PPI :
Nombre de la función: vc, supongo que representa el volumen de un cilindro.
Parámetros: r, a; al parecer r, representa el radio de una circunferencia; y a, representa la altura de un cilindro.
Retorno: el producto del area de la base por la altura, es decir el volumen del cilindro.
Objetivo: Calcular el volumen de un cilindro dados el readio y la altura del cilindro.
Reescribiendo la función quedaría así:
>>def calcular_volumen_cilindro(radio:float, altura:float)->float:
"""La funcion permite calcular el volumen de un cilindro en funcion del radio de la base y la altura del cilindro. La respuesta se redondea a dos cifras decimales"""
>> area_base = 3.14159 *(radio**2)
>> return round(area_base *altura,2)
2. Revise detenidamente la siguiente función para descubrir su objetivo. Reescribirlo aplicando las recomendaciones que se estudiaron en esta sección.
>>def v(d):
>> vf = (2*9.8*d)**(1/2)
>> return vfSolución de PPI al item 2:
>>def calcular_velocidad(distancia:float)->float:>>"""Esta función es para calcular la velocidad final de un movimiento, en función de la distancia o desplazamiento del cuerpo en metros""">> velocidad_final = (2*9.8*distancia)**(1/2) # también math.sqrt(2*9.8*distancia)>> return velocidad_final2.7.8. Más allá de Python
Comparado con otros lenguajes, el formato para la documentación de funciones en Python es relativamente pobre y desestructurado. En Java existe el formato Javadoc que es muy estructurado y permite generar automáticamente compendios con la documentación de un programa o una librería. Esquemas similares existen para otros lenguajes como JavaScript (JSDoc) y Scala (Scaladoc). Aunque no se puedan usar directamente en Python, vale la pena conocer un poco sobre las características de estos formatos (y las limitaciones que tienen) para mejorar la documentación que escribamos de las funciones Python.
La discusión sobre los type-hints tiene que ver con una discusión mucho más extensa sobre la conveniencia de tener tipado dinámico en los lenguajes de programación. Por un lado, cuando los lenguajes son fuertemente tipados se cometen menos errores o, al menos, las herramientas de edición capturan más errores de forma temprana. Por otro lado, cuando el tipado es dinámico los errores de tipo se capturan en tiempo de ejecución, pero el desarrollo de los programas es más rápido. En este momento hay fuertes discusiones sobre la conveniencia o no de cada sistema, pero hay un hecho que encontramos muy diciente: JavaScript, que tiene tipado dinámico, está incluyendo progresivamente más elementos para escribir programas fuertemente tipados (el crecimiento de TypeScript es evidencia indiscutible), mientras que Python está empezando a incluir elementos para poder incluir verificaciones de tipos.
No hay comentarios.:
Publicar un comentario