Blog - 0x00


Una plantilla simple de Python

2022/01/07

Tabla de contenidos


#

Python es un lenguaje de programación muy, si no el más, popular. Según PYPL desde 2017-2018 Python ha estado en la parte superior de la popularidad de los lenguajes de programación. Su sintaxis sencilla lo hace fácil de aprender, las funciones integradas y los tipos lo hacen fácil de transformar datos. Las universidades usan Python para enseñar programación a principiantes y cada vez que alguien quiere aprender programación pero no sabe por dónde empezar la respuesta es casi siempre Automate the Boring Stuff with Python.

Ningún lenguaje de programación es perfecto, Python o al menos la implementación de referencia (CPython) puede ser más lento que, por ejemplo, lenguajes compilados como C/C++, Rust o Go en tareas más pesadas, PERO en tareas más simples Python definitivamente tiene una ventaja, ya que la diferencia en el tiempo de ejecución es insignificante y el tiempo de desarrollo puede reducirse enormemente debido a las funciones, tipos y módulos integrados.

Últimamente he estado usando mucho Python para escribir programas/scripts y casi siempre terminaba usando una configuración similar. Cuando empiezo a escribir un nuevo script, a veces vuelvo a los antiguos para copiar parte de la configuración, razón por la cual decidí crear esta plantilla que me permitiría configurar algunos de los aspectos básicos que siempre uso, para no tener el problema de volver a configurar todo y olvidar cómo formatear con el antiguo formato %- que tiene Python para el módulo de logging.

Plantilla #

En parte me inspiré para compartir y escribir este blog debido a este blog que leí: Minimal safe Bash script template. Así que aquí está la plantilla de Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env python
'''Plantilla de Python'''
import argparse
import logging

def setup_argparse():
    '''
    Configura los argumentos para el programa

    Returns:
        args (argparse.Namespace): argumentos analizados
    '''
    parser = argparse.ArgumentParser(description="Esta es una herramienta útil que hace muchas cosas",
                                     epilog="Autor: Tomás Gutiérrez")
    parser.add_argument('-d', '--debug', action="store_true", help='establecer el nivel de log a DEBUG')
    parser.add_argument('-v', '--version', action='version', version='%(prog)s 0.1.0')
    return parser.parse_args()


def setup_logging(debug):
    '''
    Configura el módulo de logging, estableciendo el formato y nivel de log

    Parameters:
        debug (bool): si se debe mostrar el nivel debug en los logs o no
    '''
    log_level = logging.INFO
    if debug:
        log_level = logging.DEBUG
    logging.basicConfig(format="%(asctime)s [%(levelname)-8s] (%(funcName)s:%(lineno)d) %(message)s", level=log_level)


def main():
    args = setup_argparse()
    setup_logging(args.debug)

    logging.info("Script starting")
    logging.debug("Debug!")
    #### Code ####
    logging.info("Finished running script")


if __name__ == '__main__':
    main()

Esta plantilla fue probada con Python 3.7.12 y 3.10.1 y la idea detrás de esto es, en primer lugar, tenerlo organizado de forma más similar a los lenguajes de programación tradicionales donde comienza desde la función main y no solo desde cualquier línea del script, siendo capaz de registrar todo lo que sucede dentro de tu script, lo cual es útil mientras estás desarrollando para saber dónde tu programa podría estar atascándose o fallando y también una vez que lo tienes en funcionamiento y finalmente siendo capaz de añadir cualquier argumento si es necesario en lugar de tener un valor hard-coded que tienes que cambiar y guardar si quieres probar diferentes valores mientras eres capaz de controlar si quieres mostrar mensajes de debug del logger o no, lo cual es muy verboso pero cuando te encuentras con un bug es muy útil para el debug.

Shebang #

1
#!/usr/bin/env python

Esto es un shebang, se usa en archivos de texto que pueden ser ejecutados por intérpretes y le dice al sistema qué intérprete usar, en este caso Python, y sí, los archivos Python pueden tener y usar Shebangs! En lugar de ejecutar el script como $ python main.py puede ser ejecutado como $ ./main.py (Después de que le hayas dado permisos de ejecución)

Módulos #

1
2
import argparse
import logging

Argparse nos permite escribir fácilmente interfaces de línea de comandos (CLI) para que podamos añadir flags y opciones a nuestro programa cuando lo ejecutamos; más adelante en el blog explicaré cómo se configura y cómo funciona.

Logging nos permite configurar un logger. Esto puede darnos información sobre lo que el programa está haciendo y llevar un seguimiento de lo que hizo cuando se ejecuta, diferenciar los mensajes según la severidad desde informativo a crítico y otras características que proporciona este módulo.

Configuración de argparse #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def setup_argparse():
    '''
    Configura los argumentos para el programa

    Returns:
        args (argparse.Namespace): argumentos analizados
    '''
    parser = argparse.ArgumentParser(description="Esta es una herramienta útil que hace muchas cosas",
                                     epilog="Autor: Tomás Gutiérrez")
    parser.add_argument('-d', '--debug', action="store_true", help='establecer el nivel de log a DEBUG')
    parser.add_argument('-v', '--version', action='version', version='%(prog)s 0.1.0')
    return parser.parse_args()

Esta función setup_argparse() configura la configuración básica de argparse. Creamos un ArgumentParser y le damos una descripción de lo que hace nuestro script, luego podemos añadir argumentos que te permitirán añadir entradas a tu programa u opciones. En este caso solo definimos solo 2 argumentos: si queremos que los logs de debug se muestren y mostrar la versión de nuestro programa. Por defecto argparse generará una opción --help para mostrar información sobre nuestro script, incluyendo la descripción y las opciones.

Si lo dejas como está, por defecto, puedes ejecutar tu script sin ningún flag y no requerirá uno para ejecutarse, puedes dejarlo así o añadir argumentos más tarde.

Aquí tienes un ejemplo de la salida cuando se ejecuta --help:

$ ./main.py --help
usage: main.py [-h] [-d] [-v]

Esta es una herramienta útil que hace muchas cosas

options:
  -h, --help     muestra este mensaje de ayuda y sale
  -d, --debug    establecer el nivel de log a DEBUG
  -v, --version  muestra el número de versión del programa y sale

Author: Tomás Gutiérrez

Configuración de logging #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def setup_logging(debug):
    '''
    Configura el módulo de logging, estableciendo el formato y nivel de log

    Parameters:
        debug (bool): si se debe mostrar el nivel debug en los logs o no
    '''
    log_level = logging.INFO
    if debug:
        log_level = logging.DEBUG
    logging.basicConfig(format="%(asctime)s [%(levelname)-8s] (%(funcName)s:%(lineno)d) %(message)s", level=log_level)

Luego tenemos setup_logging(debug), esto nos permitirá configurar el módulo de logging. En lugar de usar print para mostrar mensajes y añadir manualmente “INFO” o “DEBUG” antes de cada mensaje y usar print() para debug, puedes usar las funciones que proporciona logging como logging.info("Información") o logging.error("¡Qué!"), no solo eso sino que cada vez que registramos se añadirá la fecha y la hora, la función donde se está ejecutando y el número de línea. El parámetro debug es un bool que proviene de la opción -d de argparse que habilitará logging.debug("debug msg") para mostrarse en la salida.

Aquí tienes un ejemplo cuando ejecutas el script:

$ ./main.py
2022-01-02 21:57:02,734 [INFO    ] (main:37) Script starting
2022-01-02 21:57:02,734 [INFO    ] (main:40) Finished running script

Aquí con los logs de debug habilitados:

$ ./main.py -d
2022-01-02 21:57:02,734 [INFO    ] (main:37) Script starting
2022-01-02 21:57:02,734 [DEBUG   ] (main:38) Debug!
2022-01-02 21:57:02,734 [INFO    ] (main:40) Finished running script

Función principal #

1
2
3
4
5
6
7
8
def main():
    args = setup_argparse()
    setup_logging(args.debug)
    
    logging.info("Script starting")
    logging.debug("Debug!")
    #### Code ####
    logging.info("Finished running script")

Luego tenemos nuestra función main, este es donde va nuestro código main, lo primero que hace es parsear los argumentos y configurar el módulo de logging y luego el programa comienza. Como marcador de posición he puesto mensajes de log. Dentro del mismo archivo main.py puedes crear más funciones y llamarlas en main() pero la idea de esto es tener un lugar donde sepamos dónde comienza y termina nuestro programa.

1
2
if __name__ == '__main__':
    main()

Finalmente tenemos esto.

Python tiene variables especiales, una de ellas siendo __name__, el valor de esta variable dependerá de cómo ejecutemos el script, si lo ejecutamos explícitamente (./main.py) su valor será '__main__', si importamos el script en otro, con import main y accedemos a ese valor con main.__name__ entonces el valor será el nombre del script (main), aquí tienes un ejemplo:

$ ls
var.py  my_script.py
$ cat var.py
import my_script

print(f"my_script.py: '{my_script.__name__}'")
print(f"var.py: '{__name__}'")
$ python var.py
my_script.py: 'my_script'
var.py: '__main__'

La mayoría de los lenguajes comienzan desde main() así que podemos hacer que Python se comporte de la misma manera para que sea más fácil saber dónde comienza el código, lo que lo hace más fácil de leer.

Pero entonces podrías preguntarte, ¿por qué querríamos comprobar si se está ejecutando directamente o si se está importando si puedo hacer lo mismo simplemente llamando a main()?

1
2
3
# if __name__ == '__main__':
#     main()
main()

Bueno, hay una diferencia entre simplemente llamar a main() y comprobar el valor de la variable especial, si en cualquier momento quieres importar main.py como un módulo en otro archivo .py entonces esta comprobación es imprescindible, de lo contrario cuando Python importa un módulo lo ejecuta y por lo tanto ejecutará main(), aquí tienes un ejemplo del comportamiento no deseado:

$ cat my_script.py
def main():
    print("Hello, I'm in my_script.py")

main()
$ cat var.py
import my_script

print("Hello, I'm in var.py")
$ python var.py
Hello, I'm in my_script.py
Hello, I'm in var.py

Definimos una función main en my_script.py que imprime un string y hacemos una llamada a main() por lo que se ejecuta main cuando se ejecuta. Luego importamos ese script como un módulo en var.py e imprimimos otro string. Cuando ejecutamos var.py primero se ejecutará my_script.py porque es lo primero que se importa, y como tenemos una llamada a main() imprimirá el string de my_script.py y luego ejecutará el print de var.py

Palabras finales #

He intentado que la plantilla sea lo más simple posible para que cualquiera pueda modificarla fácilmente según sus necesidades o extender las opciones que ya tiene, como configurar el módulo de logging para escribir en un archivo o añadir más argumentos al módulo argparse.

He creado un repositorio público en gitlab python-template que contiene el script y un README con instrucciones básicas. Siéntete libre de sugerir mejoras enviando un correo electrónico, abriendo un issue o un merge request.