Aprimorando performance e legibilidade em Python

Aprimorando performance e legibilidade em Python

A prioridade para um programador iniciante é fazer o código funcionar, mas conforme vamos avançando na carreira, percebemos que para construir projetos mais complexos é essencial se preocupar também com a performance e legibilidade do código.

Um código rápido pode economizar recursos da sua máquina e a maior legibilidade pode diminuir o tempo necessário para relembrar a estrutura do código e começar a adicionar mais funcionalidades ou resolver bugs, aprimorando o quesito da manutenção.

Nesse artigo, veremos algumas estratégias que você pode utilizar no seu código para melhorar a performance e a legibilidade.

Performance

Para aprimorar no quesito performance, podemos realizar testes de memória e velocidade com o módulo nativo time. Basta criar uma variável para o início e pegar o tempo atual, executar a função e depois pegar o tempo atual de novo e diminuir o tempo de início.

Como exemplo, vamos criar um programa para comparar o uso da função append em uma lista com uma função geradora para gerar os números de Fibonacci.

A sequência de Fibonacci é conhecida por ser uma sequência matemática que pode representar diversos fenômenos naturais, ela começa com 0 e 1 e os outros termos são derivados da soma dos dois anteriores, então 0 + 1 = 1; 1 + 1 = 2; 2 + 1 = 3; 3 + 2 = 5 e assim sucessivamente.

A função utilizando a lista vai receber um valor máximo, definir uma lista e os valores iniciais e enquanto o tamanho da quantidade de casas for menor que o número máximo, o segundo número será adicionado à lista e os próximos números da sequência serão:


Já a função utilizando geradores, vai receber o valor máximo, definir os números iniciais e um contador. Enquanto o contador for menor que o número máximo, os próximos números da sequência serão calculados, a instrução yield vai ficar esperando pelo comando next() e depois o contador será atualizado:


Agora vamos criar um laço de repetição para cada função e definir o tempo de início e calcular o tempo final. No caso do gerador, o laço de repetição é responsável por debaixo dos panos para chamar a função next():


Para saber o consumo de memória, basta executar uma função de cada vez e monitorar o consumo pelo gerenciador de tarefas caso esteja utilizando o Windows.

Em uma máquina com processador i5 de 8ª geração e 16 GB de RAM, os resultados foram que a lista demorou 350 segundos para executar e fez uso de 475.7 MB de RAM, enquanto a função geradora demorou 334 segundos e fez uso de 10.4 MB de RAM, logo, podemos concluir que a função geradora foi mais eficiente não só no quesito de tempo, como também no consumo de memória.

Legibilidade

Para aprimorar a legibilidade, podemos definir algumas regras na hora de nomear classes, funções, variáveis e constantes. As classes devem:

  • Começar com a primeira letra de cada palavra maiúscula,
  • As funções devem estar escritas em caracteres minúsculos com palavras separadas por sublinhados.

As variáveis e constantes seguem a mesma lógica das funções, a diferença é apenas nas constantes que todas as letras são maiúsculas.

Veja os exemplos abaixo:


Outro ponto que vale ser destacado é que na programação não se utilizam acentos nem caracteres especiais para nomear arquivos nem os elementos dentro do código, pois podem ocorrer problemas na hora da execução dos comandos por causa das diferentes formas de representar texto em código binário. Também é uma boa prática escrever o código em inglês, pois mais pessoas vão conseguir compreender.

Type hinting

O Python é uma linguagem de tipagem dinâmica, ou seja, quando definimos uma variável, a própria linguagem se encarrega de saber qual é o tipo daquele dado. Em outras linguagens como C, por exemplo, que são de tipagem estática, é necessário declarar o tipo de dado quando for criar a variável.

A tipagem dinâmica traz a vantagem do desenvolvedor ter uma flexibilidade maior na hora de escrever o código, porém isso também pode dificultar quando for solucionar bugs que podem vir a ocorrer, pois o tipo de dado que está em uma variável ou que é passado para uma função não é explícito.

Para solucionar esta questão, nas versões mais recentes do Python, acima do 3.5, surgiu o conceito de Type Hinting ou em português “dica de tipo”, que consiste em deixar explícito no código o tipo.

Mas vale ressaltar que essa funcionalidade não altera o comportamento da linguagem, o Python continua sendo uma linguagem de tipagem dinâmica, o que significa que você pode colocar que uma variável é do tipo string, mas passar um inteiro e isso não vai dar erro. A dica de tipo é só um lembrete para o desenvolvedor do tipo que um elemento deve receber.

Para declarar o tipo em variáveis, basta colocar dois pontos no final do nome da variável, dar um espaço, colocar o tipo de dado, depois outro espaço, o símbolo de igual e o valor dela, conforme os exemplos abaixo:


Também é possível definir tipos para os parâmetros passados para uma função, que  funciona da mesma forma que o exemplo anterior e para o retorno dela, que é feito colocando um traço e um símbolo de maior formando uma seta, depois o tipo de dado.

A partir da versão 3.8 do Python pode-se também definir o tipo em listas:


Com o Type Hinting você pode utilizar bibliotecas como o mypy que verificam os tipos para ter certeza de que estão corretos. Basta instalar a biblioteca e depois utilizar o comando mypy e o caminho com nome do arquivo no terminal:


Design Patterns

Outro conceito que ajuda na legibilidade do código em grandes projetos são os chamados padrões de projeto, ou Design Patterns. que consistem em formas de organizar o seu código para tudo ter a mesma estrutura, de modo que você não se sinta perdido dentro do sistema, pois é fácil reconhecer onde você está e como o programa deveria continuar a ser construído.

Existem diversos Design Patterns que você pode seguir. Um exemplo é a factory, onde uma classe ou função vai funcionar como uma fábrica, recebendo determinados parâmetros, executando outras instruções encapsuladas ao ser iniciada e retornando um objeto com determinados métodos.

Para exemplificar, vamos criar um programa simples que vai simular o comportamento de um servidor web e um banco de dados inicializando. Primeiro, vamos criar o arquivo main, onde haverá o seguinte código:


Aqui basicamente é importado uma classe do core do programa e todos os serviços serão inicializados apenas chamando a função start. Agora, vamos criar uma pasta chamada project e dentro dela três arquivos Python: um para o core, um para o banco de dados e outro para o servidor. Dentro do core, vamos importar as classes do banco de dados, do servidor e criar a classe para criar o core com os métodos start e stop:

Agora podemos descer mais um nível de abstração e criar a classe para criar o banco de dados com os mesmos métodos start e stop no arquivo database e outra para criar o servidor web no arquivo webserver:


Analisando o código tanto do servidor, quanto do banco de dados e do core, você pode perceber que todos têm a mesma estrutura. Todos são classes que retornam os métodos start e stop para serem inicializados, esses métodos são apenas de exemplo, mas você poderia ter vários outros e dentro do servidor e do banco de dados poderia ainda ser chamado outros métodos de classes que seriam inicializadas antes dentro do __init__ e essas classes poderiam ter o mesmo estilo.

Essa estratégia de fazer o seu código quase todo no mesmo padrão pode economizar muito tempo na hora de implementar novas funcionalidades, pois você não vai precisar ir de arquivo em arquivo analisando cada classe e cada função para saber o que eles fazem, já que tudo está padronizado.

Adotar um Design Pattern como este na construção de grandes projetos pode inclusive ajudar na hora de testar o código, utilizando o conceito de injeção de dependências, onde você pode testar cada unidade de código de forma isolada. Por exemplo, se quiséssemos testar apenas o core, poderíamos definir uma configuração passada como parâmetro para as classes do servidor e do banco de dados que só iria simular que eles estão sendo iniciados, sem de fato iniciá-los.

Dessa forma, qualquer erro que tivesse nos outros serviços seriam ignorados, pois só estamos querendo testar as funções que pertencem ao core.

Conclusão

Existem diversas formas de otimizar o seu código. Neste artigo vimos algumas das principais utilizadas na linguagem Python como a função time com a qual podemos saber o tempo de execução e consumo de memória de um bloco de código e as funções geradoras que podem ser mais eficientes em determinados casos.

Com relação aos padrões que vimos para o quesito da legibilidade, os exemplos foram feitos para a linguagem Python, mas a essência da informação de alguns como a forma de nomear as classes, funções, variáveis, constantes e arquivos, bem como os Design Patterns não se limitam pela linguagem, podem ser aplicados também em JavaScript, C++, Dart, Java, etc.

Além desses elementos descritos acima, a área da arquitetura de software, que tem como objetivo padronizar e acelerar o processo de desenvolvimento de projetos e correção de bugs, abrange também diversos outros tópicos como os princípios SOLID, que tem o propósito de facilitar no quesito da manutenção de grandes projetos e do test Driven Development que é o desenvolvimento do código a partir dos testes, dentre outros que poderemos detalhar em um próximo artigo.

Muito obrigado.

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.