Python es un lenguaje de programación que es excelente para desarrollar scripts independientes. Para lograr el resultado deseado utilizando un script similar, debe escribir varias decenas o cientos de líneas de código. Y una vez realizado el trabajo, simplemente puede olvidarse del código escrito y proceder a la solución del siguiente problema.
Si, por ejemplo, seis meses después de que se escribió un guión "único", alguien le pregunta al autor por qué falla este guión, el autor del guión puede no ser consciente de esto. Esto sucede debido al hecho de que no se escribió documentación para tal secuencia de comandos, debido al uso de parámetros codificados en el código, al hecho de que la secuencia de comandos no registra nada durante la operación, y debido a la falta de pruebas que permitieron para comprender rápidamente la causa del problema.

Cabe señalar que convertir un guión escrito a toda prisa en algo mucho mejor no es tan difícil. Es decir, un script de este tipo es bastante fácil de convertir en un código confiable y comprensible que sea conveniente de usar, en un código que sea simple de soportar tanto para su autor como para otros programadores.
El autor del material, cuya traducción publicamos hoy, demostrará tal "transformación" utilizando la
prueba clásica de
Fizz Buzz como ejemplo. Esta tarea es mostrar una lista de números del 1 al 100, reemplazando algunos de ellos con líneas especiales. Entonces, si el número es un múltiplo de 3, debe imprimir la línea
Fizz
lugar, si el número es un múltiplo de 5, la línea
Buzz
, y si se cumplen ambas condiciones,
FizzBuzz
.
Código fuente
Aquí está el código fuente de un script de Python que resuelve el problema:
import sys for n in range(int(sys.argv[1]), int(sys.argv[2])): if n % 3 == 0 and n % 5 == 0: print("fizzbuzz") elif n % 3 == 0: print("fizz") elif n % 5 == 0: print("buzz") else: print(n)
Hablemos sobre cómo mejorarlo.
La documentación
Me resulta útil escribir documentación antes de escribir el código. Esto simplifica el trabajo y ayuda a no retrasar la creación de documentación indefinidamente. La documentación para el script se puede colocar en su parte superior. Por ejemplo, podría verse así:
La primera línea da una breve descripción del propósito del guión. Los párrafos restantes proporcionan información adicional sobre lo que hace el script.
Argumentos de línea de comando
La siguiente tarea para mejorar el script será reemplazar los valores codificados en el código con los valores documentados pasados al script a través de los argumentos de la línea de comandos. Esto se puede hacer usando el módulo
argparse . En nuestro ejemplo, sugerimos al usuario que especifique un rango de números y especifique los valores de "fizz" y "buzz" que se usan al verificar números del rango especificado.
import argparse import sys class CustomFormatter(argparse.RawDescriptionHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): pass def parse_args(args=sys.argv[1:]): """Parse arguments.""" parser = argparse.ArgumentParser( description=sys.modules[__name__].__doc__, formatter_class=CustomFormatter) g = parser.add_argument_group("fizzbuzz settings") g.add_argument("--fizz", metavar="N", default=3, type=int, help="Modulo value for fizz") g.add_argument("--buzz", metavar="N", default=5, type=int, help="Modulo value for buzz") parser.add_argument("start", type=int, help="Start value") parser.add_argument("end", type=int, help="End value") return parser.parse_args(args) options = parse_args() for n in range(options.start, options.end + 1):
Estos cambios son de gran beneficio para el guión. Es decir, los parámetros ahora están debidamente documentados, puede averiguar su propósito utilizando el indicador
--help
. Además, de acuerdo con el comando correspondiente, también se muestra la documentación que escribimos en la sección anterior:
$ ./fizzbuzz.py --help usage: fizzbuzz.py [-h] [--fizz N] [--buzz N] start end Simple fizzbuzz generator. This script prints out a sequence of numbers from a provided range with the following restrictions: - if the number is divisible by 3, then print out "fizz", - if the number is divisible by 5, then print out "buzz", - if the number is divisible by 3 and 5, then print out "fizzbuzz". positional arguments: start Start value end End value optional arguments: -h, --help show this help message and exit fizzbuzz settings: --fizz N Modulo value for fizz (default: 3) --buzz N Modulo value for buzz (default: 5)
El módulo
argparse
es una herramienta muy poderosa. Si no está familiarizado con él, le será útil ver la
documentación que contiene. En particular, me gusta su capacidad para definir
subcomandos y
grupos de argumentos .
Registro
Si equipa el script con la capacidad de mostrar cierta información durante su ejecución, esto será una adición agradable a su funcionalidad. El módulo de
registro es muy adecuado para este propósito. Primero, describimos un objeto que implementa el registro:
import logging import logging.handlers import os import sys logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])
Luego, haremos posible controlar los detalles de la información que se muestra durante el registro. Entonces, el
logger.debug()
debería generar algo solo si el script se ejecuta con el
--debug
. Si el script se ejecuta con el
--silent
, el script no debe mostrar nada excepto mensajes de excepción. Para implementar estas características, agregue el siguiente código a
parse_args()
:
Agregue la siguiente función al código del proyecto para configurar el registro:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
El código del script principal cambiará de la siguiente manera:
if __name__ == "__main__": options = parse_args() setup_logging(options) try: logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) for n in range(options.start, options.end + 1):
Si planea ejecutar el script sin la participación directa del usuario, por ejemplo, usando
crontab
, puede hacer que su salida vaya a
syslog
:
def setup_logging(options): """Configure logging.""" root = logging.getLogger("") root.setLevel(logging.WARNING) logger.setLevel(options.debug and logging.DEBUG or logging.INFO) if not options.silent: if not sys.stderr.isatty(): facility = logging.handlers.SysLogHandler.LOG_DAEMON sh = logging.handlers.SysLogHandler(address='/dev/log', facility=facility) sh.setFormatter(logging.Formatter( "{0}[{1}]: %(message)s".format( logger.name, os.getpid()))) root.addHandler(sh) else: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter( "%(levelname)s[%(name)s] %(message)s")) root.addHandler(ch)
En nuestro pequeño script, parece necesaria una cantidad similar de código para usar el
logger.debug()
. Pero en las secuencias de comandos reales, este código ya no parecerá así y el beneficio de esto se pondrá en primer plano, es decir, con su ayuda, los usuarios podrán conocer el progreso de la solución del problema.
$ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz
Pruebas
Las pruebas unitarias son una herramienta útil para verificar si las aplicaciones se comportan como deberían. Los guiones de unidad se usan con poca frecuencia en los guiones, pero su inclusión en los guiones mejora significativamente la confiabilidad del código. Transformamos el código dentro del bucle en una función y describimos varios ejemplos interactivos de su uso en su documentación:
def fizzbuzz(n, fizz, buzz): """Compute fizzbuzz nth item given modulo values for fizz and buzz. >>> fizzbuzz(5, fizz=3, buzz=5) 'buzz' >>> fizzbuzz(3, fizz=3, buzz=5) 'fizz' >>> fizzbuzz(15, fizz=3, buzz=5) 'fizzbuzz' >>> fizzbuzz(4, fizz=3, buzz=5) 4 >>> fizzbuzz(4, fizz=4, buzz=6) 'fizz' """ if n % fizz == 0 and n % buzz == 0: return "fizzbuzz" if n % fizz == 0: return "fizz" if n % buzz == 0: return "buzz" return n
Puede verificar el funcionamiento correcto de la función usando
pytest
:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [100%] ========================== 1 passed in 0.05 seconds ==========================
Para que todo esto funcione, necesita la extensión
.py
después del nombre del script. No me gusta agregar extensiones a los nombres de las secuencias de comandos: el idioma es solo un detalle técnico que no necesita mostrarse al usuario. Sin embargo, parece que equipar un nombre de script con una extensión es la forma más fácil de permitir que los sistemas para ejecutar pruebas, como
pytest
, encuentren las pruebas incluidas en el código.
Si se
pytest
un error
pytest
mostrará un mensaje que indica la ubicación del código correspondiente y la naturaleza del problema:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py -k fizzbuzz.fizzbuzz ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 1 item fizzbuzz.py::fizzbuzz.fizzbuzz FAILED [100%] ================================== FAILURES ================================== ________________________ [doctest] fizzbuzz.fizzbuzz _________________________ 100 101 >>> fizzbuzz(5, fizz=3, buzz=5) 102 'buzz' 103 >>> fizzbuzz(3, fizz=3, buzz=5) 104 'fizz' 105 >>> fizzbuzz(15, fizz=3, buzz=5) 106 'fizzbuzz' 107 >>> fizzbuzz(4, fizz=3, buzz=5) 108 4 109 >>> fizzbuzz(4, fizz=4, buzz=6) Expected: fizz Got: 4 /home/bernat/code/perso/python-script/fizzbuzz.py:109: DocTestFailure ========================== 1 failed in 0.02 seconds ==========================
Las pruebas unitarias también se pueden escribir como código regular. Imagine que necesitamos probar la siguiente función:
def main(options): """Compute a fizzbuzz set of strings and return them as an array.""" logger.debug("compute fizzbuzz from {} to {}".format(options.start, options.end)) return [str(fizzbuzz(i, options.fizz, options.buzz)) for i in range(options.start, options.end+1)]
Al final del script, agregamos las siguientes pruebas unitarias utilizando las
pytest
para usar
funciones de prueba parametrizadas :
Tenga en cuenta que, dado que el código del script finaliza con una llamada a
sys.exit()
, las pruebas no se ejecutarán cuando se llame normalmente. Gracias a esto,
pytest
no
pytest
necesario para ejecutar el script.
La función de prueba se llamará una vez para cada grupo de parámetros. La entidad
args
se usa como entrada para la función
parse_args()
. Gracias a este mecanismo, obtenemos lo que necesitamos para pasar a la función
main()
. La entidad
expected
se compara con lo que
main()
.
pytest
es lo que
pytest
nos dirá si todo funciona como se esperaba:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py ============================ test session starts ============================= platform linux -- Python 3.7.4, pytest-3.10.1, py-1.8.0, pluggy-0.8.0 -- /usr/bin/python3 cachedir: .pytest_cache rootdir: /home/bernat/code/perso/python-script, inifile: plugins: xdist-1.26.1, timeout-1.3.3, forked-1.0.2, cov-2.6.0 collected 7 items fizzbuzz.py::fizzbuzz.fizzbuzz PASSED [ 14%] fizzbuzz.py::test_main[0 0-expected0] PASSED [ 28%] fizzbuzz.py::test_main[3 5-expected1] PASSED [ 42%] fizzbuzz.py::test_main[9 12-expected2] PASSED [ 57%] fizzbuzz.py::test_main[14 17-expected3] PASSED [ 71%] fizzbuzz.py::test_main[14 17 --fizz=2-expected4] PASSED [ 85%] fizzbuzz.py::test_main[17 20 --buzz=10-expected5] PASSED [100%] ========================== 7 passed in 0.03 seconds ==========================
Si se produce un error,
pytest
proporcionará información útil sobre lo que sucedió:
$ python3 -m pytest -v --doctest-modules ./fizzbuzz.py [...] ================================== FAILURES ================================== __________________________ test_main[0 0-expected0] __________________________ args = '0 0', expected = ['0'] @pytest.mark.parametrize("args, expected", [ ("0 0", ["0"]), ("3 5", ["fizz", "4", "buzz"]), ("9 12", ["fizz", "buzz", "11", "fizz"]), ("14 17", ["14", "fizzbuzz", "16", "17"]), ("14 17 --fizz=2", ["fizz", "buzz", "fizz", "17"]), ("17 20 --buzz=10", ["17", "fizz", "19", "buzz"]), ]) def test_main(args, expected): options = parse_args(shlex.split(args)) options.debug = True options.silent = True setup_logging(options) assert main(options) == expected E AssertionError: assert ['fizzbuzz'] == ['0'] E At index 0 diff: 'fizzbuzz' != '0' E Full diff: E - ['fizzbuzz'] E + ['0'] fizzbuzz.py:160: AssertionError ----------------------------- Captured log call ------------------------------ fizzbuzz.py 125 DEBUG compute fizzbuzz from 0 to 0 ===================== 1 failed, 6 passed in 0.05 seconds =====================
La salida del comando
logger.debug()
incluye en esta salida. Esta es otra buena razón para usar mecanismos de registro en scripts. Si desea saber más sobre las excelentes características de
pytest
, eche un vistazo a
este material.
Resumen
Puede hacer que los scripts de Python sean más confiables siguiendo estos cuatro pasos:
- Equipa el script con la documentación ubicada en la parte superior del archivo.
- Utilice el módulo
argparse
para documentar los parámetros con los que se puede argparse
el script. - Use el módulo de
logging
para mostrar información sobre el proceso de operación del script. - Escribir pruebas unitarias.
Aquí está el código completo para el ejemplo discutido aquí. Puede usarlo como plantilla para sus propios scripts.
Se iniciaron interesantes debates sobre este material; puede encontrarlos
aquí y
aquí . Al parecer, la audiencia recibió buenas recomendaciones sobre documentación y argumentos de la línea de comandos, pero a algunos lectores les pareció un "disparo de un arma en gorriones".
Aquí está el material que se escribió en respuesta a este artículo.
Estimados lectores! ¿Planea aplicar las recomendaciones para escribir scripts de Python que figuran en esta publicación?
