Pylint: um teste detalhado do analisador de código

Quando Luke trabalhou com Flake8 e ficou de olho em Pylint, ele teve a impressão de que 95% dos erros gerados por Pylint eram falsos. Outros desenvolvedores tinham experiência diferente na interação com esses analisadores, então Luke decidiu analisar a situação em detalhes e estudar seu trabalho em 11 mil linhas de seu código. Além disso, ele elogiou os benefícios do Pylint, vendo-o como um complemento ao Flake8.



Luke ( Luke Plant ) - um dos desenvolvedores britânicos, em cujo artigo, com a análise de analisadores de código populares, nos deparamos recentemente. Os linters estudam o código, ajudam a encontrar erros e o tornam estilisticamente consistente com os padrões e o código que os desenvolvedores da sua equipe escrevem. Os mais comuns são Pylint e Flake8. Estamos em Leader-IDTambém os usamos, então traduzimos com alegria o artigo dele. Esperamos que isso melhore sua experiência com essas ferramentas.

Configurações iniciais e base de teste


Para esse experimento, participei do código de um de meus projetos e lancei o Pylint com configurações básicas. Depois, tentou analisar o resultado: quais avisos eram úteis e quais eram falsos.

Uma pequena ajuda sobre o projeto do qual o código foi retirado:

  • Um aplicativo regular escrito em Django (ou seja, dentro do mesmo Python). O Django tem suas próprias peculiaridades e, como estrutura, possui suas limitações, mas permite escrever código Python normal. Outras bibliotecas que usam modelos (funções de retorno de chamada ou modelos de design do método de modelo) também têm algumas de suas desvantagens como estrutura.
  • Consiste em 22.000 linhas de código. Aproximadamente 11.000 linhas passaram pelo Pylint (9.000 se falhas omitidas). Essa parte do projeto consistia principalmente em visualizações e código de teste.
  • Para analisar o código desse projeto, já usei o Flake8, tendo processado todos os erros recebidos. O objetivo deste experimento foi avaliar os benefícios do Pylint como um complemento ao Flake8.
  • O projeto tem uma boa cobertura de teste do código, mas como sou seu único autor, não tive a oportunidade de usar a revisão por pares.

Espero que essa análise seja útil para outros desenvolvedores decidirem usar o Pylint na pilha para verificar a qualidade do código. Suponho que se você já estiver usando algo como o Pylint, você o usará sistematicamente para o controle de qualidade necessário do código, a fim de minimizar problemas.
Portanto, a Pylint emitiu 1650 reivindicações para o meu código.

Abaixo, agrupei todos os comentários do analisador por tipo e dei meus comentários a eles. Se você precisar de uma descrição detalhada dessas mensagens, consulte a seção apropriada da lista de funções do Pylint.

Insetos


O Pylint encontrou exatamente um bug no meu código. Considero um erro um erro que ocorre ou pode surgir potencialmente durante a operação do programa. Nesse caso, usei exceções - broad-except.isto é except Exception, não apenas a exceção, que o Flake8 captura. Isso resultaria em comportamento em tempo de execução, com algumas exceções. Se esse erro surgisse no tempo de execução (não o fato de aparecer), o comportamento incorreto do código não teria consequências sérias, no entanto ...

Total: 1

Útil


Além desse erro, Pylint encontrou mais algumas que eu categorizei como úteis. O código não caiu deles, mas pode haver problemas durante a refatoração e, em princípio, erros no futuro ao expandir a lista de recursos e dar suporte ao código.

Sete deles eram too-many-locals/ too-many-branches/ too-many-local-variables. Eles pertenciam a três partes do meu código mal estruturadas. Seria bom pensar na estrutura e tenho certeza de que poderia fazer melhor.

Outros erros:

  • unused-argument× 3 - um deles era realmente um batente, e o código foi executado corretamente aleatoriamente. Os outros dois argumentos desnecessários e não utilizados levariam a problemas no futuro se eu os usasse.
  • redefined-builtin × 2
  • dangerous-default-value × 2 - não bugs, porque nunca usei os valores padrão, mas seria bom corrigir isso por precaução.
  • stop-iteration-return × 1 - aqui eu aprendi algo novo para mim mesmo, nunca o teria encontrado.
  • no-self-argument × 1

Total: 16

Edições cosméticas


Eu prestaria menos atenção a essas coisas. Eles são insignificantes ou improváveis. Por outro lado, sua correção não será supérflua. Alguns deles são estilísticos controversos. Eu falei sobre alguns batentes semelhantes em outras seções, mas os listados aqui também são adequados para este contexto. Usando o Pylint regularmente, eu corrigia essas “falhas”, mas na maioria dos casos não me preocupo com elas.

invalid-name× 192

Estes eram basicamente nomes de variáveis ​​com uma única letra. Usado nesses contextos em que não era assustador, por exemplo:


ou


Muitos estavam no código de teste:

  • len-as-condition × 20
  • useless-object-inheritance × 16 (herdado do Python 2)
  • no-else-return × 11
  • no-else-raise × 1
  • bad-continuation × 6
  • redefined-builtin × 4
  • inconsistent-return-statements × 1
  • consider-using-set-comprehension × 1
  • chained-comparison × 1

TOTAL: 252

Sem utilidade


Foi quando eu tive motivos para escrever o código da maneira que o escrevi, mesmo que em alguns lugares pareça incomum. E, na minha opinião, é melhor deixá-lo desta forma, embora alguns não concordem ou prefiram um estilo diferente de escrever código que possa potencialmente evitar problemas.

  • too-many-ancestors × 76

Estavam no código de teste onde eu usava muitas classes de impurezas para colocar utilitários ou tocos.

  • unused-variable × 43

Foi encontrado quase o tempo todo no código de teste, onde eu quebrei o recorde:


... e não usou nenhum dos elementos. Existem várias maneiras de impedir que o Pylint relate erros (por exemplo, forneça nomes unused). Mas se você deixar na forma em que escrevi, será legível e as pessoas (inclusive eu) serão capazes de entendê-lo e apoiá-lo.

  • invalid-name × 26

Esses foram os casos em que designei nomes apropriados no contexto, mas aqueles que não atendiam aos padrões de nomenclatura Pylint. Por exemplo, db (esta é uma abreviação comum para banco de dados) e alguns outros nomes não-padrão, que, na minha opinião, eram mais compreensíveis. Mas você pode não concordar comigo.

  • redefined-outer-name × 16

Às vezes, o nome de uma variável é digitado corretamente nos contextos interno e externo. E você nunca precisará usar um nome externo de um contexto interno.

  • too-few-public-methods × 14

Os exemplos incluem classes com dados criados usando attrs , onde não há métodos públicos, e uma classe que implementa a interface do dicionário, mas que é necessária para garantir que o método funcione corretamente.__getitem__

  • no-self-use × 12

Eles apareceram no código de teste, onde adicionei intencionalmente métodos à classe base sem um parâmetro self, pois dessa maneira era mais conveniente importá-los e disponibilizá-los para o caso de teste. Alguns deles até envolvem funções separadas.

  • attribute-defined-outside-init × 10

Nesses casos, havia boas razões para escrever o código como está. Basicamente, esses erros ocorreram no código de teste.

  • too-many-locals× 6, too-many-return-statements× 6, too-many-branches× 2, too-many-statements× 2

Sim, esses recursos eram muito longos. Mas, olhando para eles, não vi boas maneiras de limpá-los e melhorá-los. Um dos recursos era simples, embora longo. Tinha uma estrutura muito clara e não foi escrita de forma torta, e qualquer maneira de reduzi-la em que eu pudesse pensar incluiria camadas inúteis de funções desnecessárias ou inconvenientes.

  • arguments-differ × 6

Isso se deve principalmente ao uso de * args e ** kwargs em um método substituído, o que permite que você se proteja de alterações na assinatura de métodos de bibliotecas de terceiros (mas, em alguns casos, essa mensagem pode indicar erros reais).

  • ungrouped-imports × 4

Eu já uso o isort para importar

  • fixme × 4

Sim, há várias coisas que precisam ser corrigidas, mas no momento não quero corrigi-las.

  • duplicate-code × 3

Às vezes, você usa uma pequena quantidade de código padrão, o que é simplesmente necessário e, quando não há muita lógica real no corpo da função, esse aviso é acionado.

  • broad-except × 2
  • abstract-method × 2
  • redefined-builtin × 2
  • too-many-lines × 1

Eu tentei descobrir de que maneira natural quebrar esse módulo, mas não consegui. Este é um exemplo em que você pode ver que o linter é a ferramenta errada. Se eu tiver um módulo com 980 linhas de código e adicionar mais 30, cruzo o limite de 1000 linhas e as notificações do linter não me ajudarão aqui. Se 980 linhas são boas, por que 1010 é ruim? Não quero refatorar este módulo, mas quero que o linter não produza erros. A única solução neste momento que vejo é fazer de alguma forma um modo de silenciar o ponteiro, o que contradiz o objetivo final.

  • pointless-statement × 1
  • expression-not-assigned × 1
  • cyclic-import × 1

Nós descobrimos o ciclo movendo suas partes para uma das funções. Não consegui encontrar uma maneira melhor de estruturar o código com base em restrições.

  • unused-import × 1

Eu já adicionei # NOQAao usar o Flake8 para que esse erro não apareça.

  • too-many-public-methods × 1

Se na minha classe de teste existem 35 testes em vez de 20 regulamentados, isso é realmente um problema?

  • too-many-arguments × 1

Total: 243

Não foi possível corrigir


Essa categoria reflete erros que não posso corrigir, mesmo que eu quisesse, devido a restrições externas, como a necessidade de retornar ou passar classes para uma biblioteca ou estrutura de terceiros que devem atender a certos requisitos.

  • unused-argument × 21
  • invalid-name × 13
  • protected-access × 3

Acesso incluído a "objetos internos documentados", como sys._getframeno stdlib e no Django Model._meta.

  • too-few-public-methods × 3
  • too-many-arguments × 2
  • wrong-import-position × 2
  • attribute-defined-outside-init × 1
  • too-many-ancestors × 1

Total: 46

Mensagens falsas


Coisas nas quais Pylint está claramente errado. Nesse caso, esses não são erros do Pylint: o fato é que o Python é dinâmico e o Pylint está tentando descobrir coisas que não podem ser feitas de maneira perfeita ou confiável.

  • no-member × 395

Associado a várias classes base: do Django e aquelas que eu mesmo criei. Pylint não conseguiu detectar a variável devido ao dinamismo / metaprogramação.

Muitos erros ocorreram devido à estrutura do código de teste (usei um modelo do django-functest, que em alguns casos pode ser corrigido adicionando classes base adicionais usando métodos "abstratos" que chamam NotImplementedError) ou, possivelmente, renomeando muitas classes de teste (I Eu não fiz isso, porque em alguns casos seria confuso).

  • invalid-name × 52

O problema surgiu principalmente porque a Pylint aplicou a regra constante do PEP8 , considerando que todo nome de nível superior definido com =é uma "constante". Definir exatamente o que entendemos por constante é mais difícil do que parece, mas isso não se aplica a algumas coisas que são inerentemente constantes, por exemplo, funções. Além disso, a regra não deve ser aplicada a formas menos familiares de criar funções, por exemplo:


Alguns exemplos são discutíveis devido à falta de definição do que é uma constante. Por exemplo, uma instância de uma classe definida no nível do módulo, que pode ou não ter um estado mutável, deve ser considerada constante? Por exemplo, nesta expressão:


  • no-self-use × 23

O método Pylint indicado incorretamente pode ser uma função para muitos casos em que uso herança para executar várias implementações, respectivamente, não posso convertê-las em funções.

  • protected-access × 5

O Pylint avaliou incorretamente quem era o "proprietário" (o fragmento de código atual cria o atributo protegido do objeto e o utiliza localmente, mas o Pylint não vê isso).

  • no-name-in-module × 1
  • import-error × 1
  • pointless-statement × 1

Esta declaração realmente produziu o resultado:


Eu usei isso para causar intencionalmente um erro incomum que é improvável que seja encontrado pelos testes. Não culpo Pylint por não o reconhecer ...

Total: 477

Subtotal


Ainda não estamos na linha de chegada, mas é hora de agrupar nossos resultados:

  1. Blocos “Bom” - “Bugs” e “Utilitários” - aqui o Pylint definitivamente ajudou: 17.
  2. “Neutro” - “Alterações cosméticas” - um benefício menor do Pylint, os erros não causarão danos: 252.
  3. “Ruim” - “Inútil”, “Não é possível corrigir”, “Imprecisões” - onde o Pylint deseja alterações no código, onde não é necessário. Incluindo onde as edições não podem ser feitas devido a dependências externas ou onde a Pylint simplesmente analisou incorretamente o código: 766.

A proporção de bom para ruim é muito pequena. Se Pylint fosse meu colega ajudando na revisão do código, eu imploraria para que ele fosse embora.

Para remover notificações falsas, você pode abafar a classe de erro de saída (que aumentará a inutilidade do uso do Pylint) ou adicionar comentários especiais ao código. Eu não quero fazer o último, porque:

  1. Leva tempo!
  2. Não gosto de acumular comentários que existem para silenciar o linter.

De bom grado, adicionarei esses pseudo-comentários quando houver uma vantagem definitiva do linter. Além disso, estou ansioso por comentários, portanto, o destaque da minha sintaxe os exibe de maneira vívida: conforme recomendado neste artigo . No entanto, em alguns lugares, eu já adicionei # comentários NOQApara abafar o Flake8, mas com eles para uma seção você pode adicionar apenas cinco códigos de erro.

Docstrings


Os problemas remanescentes descobertos pela Pylint estavam faltando linhas de documentação. Coloquei-os em uma categoria separada porque:

  1. Eles são muito controversos e você pode ter uma política completamente diferente em relação a essas coisas.
  2. Não tive tempo para analisar todos eles.

No total, a Pylint descobriu 620 dockstrings ausentes (em módulos, funções, métodos de classe). Mas em muitos casos, eu estava certo. Por exemplo:

  • Quando tudo estiver claro a partir do nome. Por exemplo:
  • Quando uma docstring já está definida - por exemplo, se eu implementar a interface do roteador de banco de dados do Django . Adicionar sua própria linha neste caso pode ser perigoso.

Em outros casos, essas linhas de descrição não prejudicariam meu código. Em cerca de 15 a 30% dos casos encontrados por Pylint, eu pensaria: "Sim, preciso adicionar uma doutrina aqui, obrigado Pylint pelo lembrete".

Em geral, não gosto das ferramentas que fazem a adição de dockstrings em qualquer lugar e em qualquer lugar, porque acho que nesse caso elas ficarão ruins. É melhor não escrevê-los. Uma situação semelhante com comentários ruins:

  • lê-los é uma perda de tempo: eles não têm informações adicionais ou contêm informações incorretas,
  • eles ajudam subconscientemente a destacar docstrings no texto, porque contêm informações úteis (se você as escrever no caso).

Os avisos sobre linhas de documentação ausentes são irritantes: para removê-los, é necessário adicionar comentários separadamente, o que leva aproximadamente a mesma quantidade de tempo que a adição do próprio encaixe. Além disso, tudo cria ruído visual. Como resultado, você obterá linhas de documentação que não são necessárias ou úteis.

Conclusão


Acredito que minhas suposições iniciais sobre a inutilidade do Pylint se mostraram corretas (no contexto da base de código para a qual eu uso o Flake8). Para que eu possa usá-lo, o indicador que reflete o número de falsos positivos deve ser menor.

Além de criar ruído visual, é necessário mais tempo para classificar ou filtrar as notificações falsas, e eu não gostaria de adicionar nada disso ao projeto. Os desenvolvedores juniores serão mais humildes ao fazer edições para remover os comentários de Pylint. Mas isso fará com que eles quebrem o código de trabalho, não percebam que o Pylint está errado ou que, como resultado, você terá muita refatoração para que o Pylint possa entender seu código corretamente.

Se você usa o Pylint em um projeto desde o início ou em um projeto onde não há dependências externas, acho que você terá uma opinião diferente e o número de notificações falsas será menor. No entanto, isso pode levar a custos de tempo adicionais.

Outra abordagem é usar o Pylint para um número limitado de tipos de erros. No entanto, havia apenas algumas, cujas respostas se mostraram corretas ou extremamente raramente falsas (em termos relativos e absolutos). Entre eles estão: dangerous-default-value, stop-iteration-return, broad-exception, useless-object-inheritance.

De qualquer forma, espero que este artigo tenha ajudado você a considerar usar Pylint ou em uma disputa com colegas.

All Articles