Python é uma linguagem de programação excelente para o desenvolvimento de scripts independentes. Para alcançar o resultado desejado usando um script semelhante, você precisa escrever várias dezenas ou centenas de linhas de código. E depois que o trabalho estiver concluído, você pode simplesmente esquecer o código escrito e prosseguir para a solução do próximo problema.
Se, digamos, seis meses após a gravação de um certo script "único", alguém perguntar ao autor o motivo pelo qual esse script falhou, o autor do script pode não estar ciente disso. Isso ocorre devido ao fato de nenhuma documentação ter sido gravada para esse script, devido ao uso de parâmetros codificados no código, devido ao fato de o script não registrar nada durante a operação e à falta de testes que permitiram para entender rapidamente a causa do problema.

Note-se que transformar um script escrito às pressas em algo muito melhor não é tão difícil. Nomeadamente, é muito fácil transformar esse script em um código confiável e compreensível, conveniente de usar, em um código simples para dar suporte ao autor e a outros programadores.
O autor do material, cuja tradução publicamos hoje, demonstrará essa "transformação" usando o clássico
Fizz Buzz Test como exemplo. Esta tarefa é exibir uma lista de números de 1 a 100, substituindo alguns deles por linhas especiais. Portanto, se o número for múltiplo de 3, você precisará imprimir a linha
Fizz
, se o número for múltiplo de 5, a linha
Buzz
e, se essas duas condições forem atendidas, o
FizzBuzz
.
Código fonte
Aqui está o código fonte de um script Python que resolve o 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)
Vamos falar sobre como melhorá-lo.
A documentação
Acho útil escrever documentação antes de escrever o código. Isso simplifica o trabalho e ajuda a não atrasar a criação da documentação indefinidamente. A documentação para o script pode ser colocada na parte superior. Por exemplo, pode ser assim:
A primeira linha fornece uma breve descrição do objetivo do script. Os parágrafos restantes fornecem informações adicionais sobre o que o script faz.
Argumentos da linha de comando
A próxima tarefa para aprimorar o script será substituir os valores codificados no código pelos valores documentados transmitidos ao script por meio dos argumentos da linha de comando. Isso pode ser feito usando o módulo
argparse . Em nosso exemplo, sugerimos que o usuário especifique um intervalo de números e especifique os valores para "fizz" e "buzz" usados ao verificar números do intervalo 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):
Essas mudanças são de grande benefício para o script. Nomeadamente, os parâmetros agora estão devidamente documentados, você pode descobrir sua finalidade usando o sinalizador
--help
. Além disso, de acordo com o comando correspondente, a documentação que escrevemos na seção anterior também é exibida:
$ ./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)
O módulo
argparse
é uma ferramenta muito poderosa. Se você não estiver familiarizado, será útil examinar a
documentação . Gosto particularmente da capacidade dele de definir
subcomandos e
grupos de argumentos .
Registo
Se você equipar o script com a capacidade de exibir algumas informações durante sua execução, isso será uma adição agradável à sua funcionalidade. O módulo de
registro é adequado para essa finalidade. Primeiro, descrevemos um objeto que implementa o log:
import logging import logging.handlers import os import sys logger = logging.getLogger(os.path.splitext(os.path.basename(sys.argv[0]))[0])
Em seguida, possibilitaremos o controle dos detalhes das informações exibidas durante o registro. Portanto, o comando
logger.debug()
deve
logger.debug()
algo apenas se o script for executado com a opção
--debug
. Se o script for executado com a
--silent
, o script não deverá exibir nada, exceto mensagens de exceção. Para implementar esses recursos, adicione o seguinte código a
parse_args()
:
Adicione a seguinte função ao código do projeto para configurar o log:
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)
O código do script principal será alterado da seguinte maneira:
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):
Se você planeja executar o script sem a participação direta do usuário, por exemplo, usando o
crontab
, você pode fazer sua saída ir para
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)
Em nosso pequeno script, uma quantidade semelhante de código parece necessária para usar apenas o comando
logger.debug()
. Mas, em scripts reais, esse código não parecerá mais isso e os benefícios surgirão em primeiro plano, ou seja, com a ajuda dos usuários, eles poderão descobrir o progresso da solução do problema.
$ ./fizzbuzz.py --debug 1 3 DEBUG[fizzbuzz] compute fizzbuzz from 1 to 3 1 2 fizz
Testes
Os testes de unidade são uma ferramenta útil para verificar se os aplicativos se comportam como deveriam. Os scripts de unidade são usados com pouca frequência nos scripts, mas sua inclusão nos scripts melhora significativamente a confiabilidade do código. Transformamos o código dentro do loop em uma função e descrevemos vários exemplos interativos de seu uso em sua documentação:
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
Você pode verificar a operação correta da função 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 tudo isso funcione, você precisa da extensão
.py
após o nome do script. Não gosto de adicionar extensões aos nomes dos scripts: o idioma é apenas um detalhe técnico que não precisa ser mostrado ao usuário. No entanto, parece que equipar um nome de script com uma extensão é a maneira mais fácil de permitir que sistemas para executar testes, como
pytest
, encontrem os testes incluídos no código.
Se
pytest
um erro
pytest
exibirá uma mensagem indicando a localização do código correspondente e a natureza do 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 ==========================
Os testes de unidade também podem ser escritos como código regular. Imagine que precisamos testar a seguinte função:
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)]
No final do script, adicionamos os seguintes testes de unidade usando os
pytest
para usar
funções de teste parametrizadas :
Observe que, como o código do script termina com uma chamada para
sys.exit()
, os testes não serão executados quando for chamado normalmente. Graças a isso, o
pytest
não
pytest
necessário para executar o script.
A função de teste será chamada uma vez para cada grupo de parâmetros. A entidade
args
é usada como entrada para a função
parse_args()
. Graças a esse mecanismo, obtemos o que precisamos passar para a função
main()
. A entidade
expected
é comparada com o que
main()
. Aqui está o que o
pytest
nos dirá se tudo funcionar como esperado:
$ 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 ==========================
Se ocorrer um erro, o
pytest
fornecerá informações úteis sobre o que aconteceu:
$ 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 =====================
A saída do comando
logger.debug()
está incluída nesta saída. Esse é outro bom motivo para usar mecanismos de log em scripts. Se você quiser saber mais sobre os excelentes recursos do
pytest
, dê uma olhada
neste material.
Sumário
Você pode tornar os scripts Python mais confiáveis seguindo estas quatro etapas:
- Equipe o script com a documentação localizada na parte superior do arquivo.
- Use o módulo
argparse
para documentar os parâmetros com os quais o script pode ser chamado. - Use o módulo de
logging
para exibir informações sobre o processo de operação do script. - Escreva testes de unidade.
Aqui está o código completo do exemplo discutido aqui. Você pode usá-lo como um modelo para seus próprios scripts.
Discussões interessantes começaram em torno deste material - você pode encontrá-las
aqui e
aqui . O público, ao que parece, recebeu bem recomendações sobre documentação e argumentos de linha de comando, mas o que dizer de registros e testes pareceu a alguns leitores ser um "tiro de um canhão em pardais"?
Aqui está o material que foi escrito em resposta a este artigo.
Caros leitores! Você planeja aplicar as recomendações para escrever scripts Python fornecidos nesta publicação?
