Performance y legibilidad en Python
La prioridad para un programador principiante es hacer que el código funcione, pero a medida que avanzamos en nuestra carrera, nos damos cuenta de que, para construir proyectos más complejos, es fundamental preocuparse también por el rendimiento y la legibilidad del código.
El código rápido puede ahorrar recursos en su máquina y una mayor legibilidad puede disminuir el tiempo necesario para recordar la estructura del código y comenzar a agregar más funcionalidades o corregir errores, mejorando el aspecto de mantenimiento.
En este artículo, veremos algunas estrategias que puede usar en su código para mejorar el rendimiento y la legibilidad.
Performance
Para mejorar el rendimiento, podemos realizar pruebas de memoria y velocidad con el tiempo del módulo nativo. Simplemente cree una variable para el inicio y obtenga la hora actual, ejecute la función y luego obtenga la hora actual nuevamente y disminuya la hora de inicio.
Como ejemplo, vamos a crear un programa para comparar utilizando la función de agregar en una lista con una función generadora para generar números de Fibonacci.
La Secuencia de Fibonacci es conocida por ser una secuencia matemática que puede representar varios fenómenos naturales, comienza con 0 y 1 y los demás términos se derivan de la suma de los dos anteriores, así que 0 + 1 = 1; 1 + 1 = 2; 2 + 1 = 3; 3 + 2 = 5 y así sucesivamente.
La función que usa la lista recibirá un valor máximo, definirá una lista y los valores iniciales y mientras el tamaño del número de casas sea menor que el número máximo, el segundo número se agregará a la lista y los siguientes números en la secuencia serán:
La función que utiliza generadores, en cambio, recibirá el valor máximo, definirá los números iniciales y un contador. Mientras el contador sea menor que el número máximo, se calcularán los siguientes números en la secuencia, la instrucción yield esperará el comando next() y luego se actualizará el contador:
Ahora vamos a crear un loop para cada función y definir la hora de inicio y calcular la hora de finalización. En el caso del generador, el loop es responsable detrás de escena de llamar a la función next():
Para conocer el consumo de memoria, simplemente ejecute una función a la vez y controle el consumo a través del administrador de tareas si está utilizando Windows.
En una máquina con un procesador i5 de octava generación y 16 GB de RAM, los resultados fueron que la lista tardó 350 segundos en ejecutarse y usó 475,7 MB de RAM, mientras que la función generador tardó 334 segundos y usó 10,4 MB de RAM. Por lo tanto, puede concluir que la función generadora fue más eficiente no solo en términos de tiempo, sino también en términos de consumo de memoria.
Legibilidad
Para mejorar la legibilidad, podemos definir algunas reglas al nombrar clases, funciones, variables y constantes. Las clases deben:
- Comenzar con la primera letra de cada palabra en mayúscula,
- Las funciones deben escribirse en minúsculas con palabras separadas por guiones bajos.
Las variables y las constantes siguen la misma lógica que las funciones, la única diferencia es que las constantes tienen todas las letras en mayúsculas.
Ve los ejemplos a continuación:
Otro punto a destacar es que en la programación no se utilizan acentos o caracteres especiales para nombrar archivos o elementos dentro del código, porque pueden presentarse problemas a la hora de ejecutar comandos debido a las diferentes formas de representar el texto en código binario.
También es una buena práctica escribir el código en inglés, ya que más personas podrán entenderlo.
Type Hinting
Python es un lenguaje tipado dinámicamente, es decir, cuando definimos una variable, el propio lenguaje se encarga de saber qué tipo de dato es. En otros lenguajes como C, por ejemplo, que son de tipo estático, es necesario declarar el tipo de datos al crear la variable.
El tipeado dinámico trae la ventaja de que el desarrollador tiene mayor flexibilidad a la hora de escribir el código, pero esto también puede dificultar la resolución de bugs que puedan ocurrir, porque el tipo de dato que está en una variable o que se pasa a una función no es explícito.
Para solucionar este problema, en las versiones más recientes de Python, por encima de la 3.5, apareció el concepto de Type Hinting o Guía de tipo que consiste en hacer explícito el tipo en el código.
Pero vale la pena mencionar que esta funcionalidad no cambia el comportamiento del lenguaje, Python sigue siendo un lenguaje de tipo dinámico, lo que significa que puedes especificar que una variable es de tipo string, pero pasa un número entero y no dará error. La sugerencia de tipo es solo un recordatorio para el desarrollador de qué tipo se debe dar a un elemento.
Para declarar el tipo en variables, basta con colocar dos puntos al final del nombre de la variable, agregar un espacio, colocar el tipo de dato, luego otro espacio, el símbolo igual y su valor, según los ejemplos a continuación:
También es posible definir tipos para los parámetros pasados a una función, que funciona de la misma forma que el ejemplo anterior y para su retorno, que se hace colocando un guión y un símbolo mayor formando una flecha, luego el tipo de dato.
A partir de Python 3.8, también puedes definir el tipo en las listas:
Con Type Hinting puedes usar bibliotecas como mypy que verifican los tipos para asegurarse de que sean correctos. Simplemente instala la biblioteca y luego usa el comando mypy y la ruta con el nombre del archivo en la terminal:
Design Patterns
Otro concepto que ayuda a la legibilidad del código en grandes proyectos son los llamados Patrones de Diseño o Design Patterns, que consisten en formas de organizar tu código para que todo tenga la misma estructura, para que no te sientas perdido dentro del sistema, ya que es fácil reconocer dónde estás y cómo se debe seguir construyendo el programa.
Hay varios patrones de diseño que puedes seguir. Un ejemplo es la fábrica, donde una clase o función funcionará como una fábrica, recibiendo ciertos parámetros, ejecutando otras instrucciones encapsuladas cuando se inicie y devolviendo un objeto con ciertos métodos.
Para ejemplificar, vamos a crear un programa simple que simula el comportamiento de un servidor web y una base de datos que se inicializa. Primero, vamos a crear el archivo principal, donde estará el siguiente código:
Aquí, básicamente se importa una clase del core del programa y todos los servicios se iniciarán simplemente llamando a la función start. Ahora, vamos a crear una carpeta llamada project y dentro de ella tres archivos de Python: uno para el core, uno para la base de datos y otro para el servidor.
Dentro del core, importemos la base de datos y las clases del servidor y creemos la clase para crear el núcleo con los métodos de start y stop:
Ahora podemos bajar un nivel más de abstracción y crear la clase para crear la base de datos con los mismos métodos de start y stop en el archivo de la base de datos y otro para crear el servidor web en el archivo webserver:
Analizando el código tanto del servidor como de la base de datos y del core, se puede ver que todos tienen la misma estructura. Todas son clases que devuelven métodos de start y stop para ser inicializados, estos métodos son solo un ejemplo, pero podrías tener varios otros y dentro del servidor y la base de datos se podrían llamar otros métodos de clases que se inicializarían antes dentro del __init__ y estas clases podrían tener el mismo estilo.
Esta estrategia de hacer tu código casi todo en el mismo patrón puede ahorrarte mucho tiempo a la hora de implementar nuevas funcionalidades, ya que no necesitarás ir de archivo en archivo analizando cada clase y cada función para saber qué hacen, ya que todo está estandarizado.
Adoptar un Design Pattern como éste al crear proyectos grandes puede incluso ayudar al probar el código, utilizando el concepto de inyección de dependencia, donde puedes probar cada unidad de código de forma aislada. Por ejemplo, si solo quisiéramos probar el núcleo, podríamos definir una configuración que se pasa como parámetro al servidor y las clases de la base de datos que solo simularían que se están iniciando, sin que realmente se iniciaran, ya que solo queremos probar las funciones que pertenecen al core.
Conclusión
Hay varias formas de optimizar su código. En este artículo vimos algunas de las principales que se utilizan en el lenguaje Python como la función time con la que podemos saber el tiempo de ejecución y el consumo de memoria de un bloque de código y las funciones generadoras que pueden ser más eficientes en determinados casos.
En cuanto a los patrones que vimos para el tema de la legibilidad, los ejemplos fueron hechos para el lenguaje Python, pero la esencia de la información de algunos como la forma de nombrar clases, funciones, variables, constantes y archivos, así como los Patrones de Diseño no son están limitados por el idioma, también se pueden aplicar en JavaScript, C++, Dart, Java, etc.
Además de estos elementos descritos anteriormente, el área de arquitectura de software, que tiene como objetivo estandarizar y acelerar el proceso de desarrollo de proyectos y corrección de errores, también cubre varios otros temas como los principios SOLID, que tienen como objetivo facilitar en términos de mantenimiento de grandes proyectos y del test Driven Development, que es el desarrollo de código a partir de pruebas, entre otros que podremos detallar en un próximo artículo.
Muchas gracias.
Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.