Mudando de Java para Python

Sim, não é fácil, mas também não é complicado

Willian Antunes
Juntos Somos Mais
12 min readJan 26, 2019

--

Sair da zona de conforto normalmente tem mais prós do que contras, só que os contras podem tomar proporções gigantescas. Eu costumo pensar muito sobre essa questão quando tenho oportunidade de algo. Eis a situação que caiu sobre mim: reescrever um projeto para Python, para aprender de verdade. Mas como assim, “aprender de verdade”? O que quero dizer é que ficar só no campo do estudo lendo livros, fazendo cursos, etc., não é tão proveitoso quanto pegar um projeto de verdade para lidar com problemas reais.

“Experimentando” várias linguagens, no final a sensação é tipo assim

Motivo real da reescrita

Fiz um projeto em Kotlin com Apache Camel (inclusive comentei um pouco dele aqui) para um determinado fim, mas que tinha que ser trabalhado mais para poder concluí-lo efetivamente. Eu já tinha alinhado com o time que os projetos subsequentes seriam todos em Python, mas aí vários requisitos começaram a surgir, além dos que eu tinha estimado, e assim comecei a questionar com o aumento da demanda:

Terminar com o que eu tinha começado ou “meter o biruta”, reescrevendo tudo para Python e sair da zona de conforto o quanto antes?

Bom, como eu precisava aprende-lo de uma maneira ou de outra para a próxima missão, decidi então pela reescrita, e sim, foi uma aventura. Segue um repositórios que fiz para testar recursos da linguagem:

Fruto de lições aprendidas da reescrita, seguem tópicos importantes para quem vai passar ou já está passando por situação similar… Sair temporariamente da JVM e ir para Python!

Comparando um pouco com o mundo Java

Quem está dominando quem?

Tem muita coisa que comparar, mas decidi focar no que entendo ser essencial. Como existe muita convenção “fixa” no mundo Java, dificilmente você passará por complicações do tipo:

  1. Onde armazenar o código-fonte que tem a regra de negócio concreta?
  2. Qual diretório manter os testes unitários?
  3. Como resolver conflitos de dependências?

O exemplo 3 nem sempre é garantido pelo Maven/Gradle pois dependendo do caso alguma intervenção manual ainda é necessária, só que enfim, como é com Python esse e outros pontos? Vejamos.

Método de execução principal

Em Java temos o famoso public static void main(String[] args), esse é o ponto de entrada de execução principal do seu projeto. É uma abordagem errada, mas podemos ter vários deles em nosso programa, porque quem vai definir qual será executado é o nosso arquivo de manifesto.

Em Python, definimos a mesma coisa da seguinte maneira:

Sim, parece estranho, mas vamos ao entendimento. Assim como em Java, você pode colocá-lo em todos os módulos (arquivos *.py) do seu programa, só que não existe arquivo de manifesto, pois é uma linguagem script, então como definimos? Simplesmente nós devemos definir qual arquivo executar ao rodar nosso serviço, por exemplo a seguinte linha de comando:

Dessa maneira todos os outros que tem a entrada de execução principal serão ignorados pois o bloco condicional não retornará verdadeiro. A variável especial __name__ terá o valor __main__ somente se ele for o ponto de entrada na execução do comando Python, assim como definimos no exemplo antes. Se você debugar, verá que nos outros módulos a variável especial __name__ será o nome do próprio arquivo.

Algo interessante é que o IntelliJ identifica que é o ponto de execução principal, assim como é em Java, olha só:

IntelliJ destacando execução

Sugiro a leitura da postagem do Guido, criador do Python, sobre o tema.

Sistema de importação

Em Java, quando precisamos usar uma classe e algo estático, usamos import e import static, respectivamente. A partir desse momento, supondo um cenário onde importamos A que tem apenas a construtora padrão e o método estático B.doTheThing, podemos então instanciar A e executar o método estático sem referenciar B no código. Exemplo:

Simples até de mais até aqui. Em Python é super diferente: quando importamos um módulo (arquivos *.py são entendidos assim) usando import, o interpretador da linguagem executará todo o conteúdo dele, então se você instanciar uma classe, definir funções, constantes, enfim, tudo ficará vinculado ao nome do módulo no import. Vamos considerar esse módulo:

E um exemplo de uso dele:

Por incrível que pareça, estamos executando o tal do sample_module só pelo import. Olha a saída no console do que foi executado:

Só aí dá para ter noção da diferença brutal frente ao Java. A instância do módulo para consumo no main é feita uma única vez em todo o programa, ou seja, se outro módulo usar import sample.test.sample_module ou from sample.test import sample_module (maneira diferente de importar por ser mais específico em alguns casos), sua instância não será feita novamente, uma vez que já foi feito em outro ponto do código, considerando nosso exemplo, não será impresso line 6, line 10, line 17 e line 21 novamente.

IntelliJ mostrando conteúdo do sample_module

Um ponto super importante: se você conhece um pouco de Node.js, sim, o funcionamento é análogo ao sistema de módulos CommonJS, a diferença é que você não precisa executar module.exports para explicitar o que deve ser exportado para quem importar aquele determinado módulo.

Conheça mais dos pormenores na documentação:

Tipagem dinâmica com type hint

Provavelmente você tenha desenvolvido algo com JavaScript. É praticamente igual ao Python falando em tipagem, com a diferença que rola type hint. Veja um exemplo aplicado com parâmetros e retorno da função:

Isso ajuda no Code Completion, facilitando o desenvolvimento, inclusive a leitura do código em determinadas situações. Digo em determinadas situações pois o propósito da linguagem é diferente, então colocar tipagem em tudo quanto e lado talvez não faça sentido. Sugiro a leitura da documentação sobre o tema:

Um dos motivos do sucesso do TypeScript com certeza é isso, mas lembra-se: em Python é só uma dica, longe de ser uma tipagem de verdade, bem longe, embora você possa usar um static type checker.

Trabalhando sem injeção de dependências

Como vimos, o sistema de importação do Python já cria tudo quando realizamos o primeiro import de um dado objeto, obtendo do “cache” (licença poética) quando outro módulo o usa. Com essa abordagem, o uso de DI pode ser deixado de lado em vários sentidos, embora pareça esquisito em um primeiro momento, o costume só vem com a prática. Por exemplo eu gosto de fazer tudo com funções, sem classes, na verdade uso classe no máximo para definir dataclass (muito legal para trabalhar com DTO em vez de dicionários ou tuplas nomeadas), assim, isolando bem as responsabilidades, fica tranquilo trabalhar sem injeção de dependências, contudo tome cuidado: funciona bem para mim, então pode ser diferente para você.

Sugiro a leitura da discussão sobre o tema no StackOverflow abaixo, não esqueça dos comentários nas respostas (por exemplo essa aqui), enriquecedor!

Adicionando uma dependência no projeto

Quando usamos Maven, a dependência é salva localmente na pasta .m2/repository do seu usuário, prevenindo downloads desnecessários no futuro, e o que deve ser usado no projeto é especificado no arquivo pom.xml, embora o pacote fique disponível para uso globalmente, o projeto só enxergará o que está descrito no POM, nada mais, além disso, o que é baixado pelo Maven não fica disponível por linha de comando, agora com pip o jogo é bem diferente.

Tudo que você instala por ele é visível para todos os projetos Python da sua máquina (lembra-se, é uma linguagem script), não tem como você especificar qual versão exata ele deve usar de uma dada biblioteca (calma, vamos resolver essa questão), pois ele faz simplesmente o import, despreocupando-se com a versão, isso é ruim por vários motivos:

  • Conflitos: um projeto pode depender da versão 4 do pacote salt e outro da versão 3. Se a instalação é global, logo você terá que desinstalar e instalar uma versão e outra em função de um projeto ou outro.
  • Complexidade: versão de pacote é uma coisa, e a versão do Python em si? Um projeto pode depender da versão 3.1 e outro da 3.7, se é global, workarounds birutaços são necessários.
  • Poluição: Com os exemplos acima, fica fácil imaginar a poluição que o seu ambiente global possa ficar.

Quando instalar uma lib, por exemplo pip install pytest, saiba que ele baixará a última versão, ela estará visível globalmente e também ficará disponível no seu shell (tente executar pytest após a instalação).

Resumindo, sem workaround, usando somente o pip, não tem como manter o uso de pacote por projeto assim como é com Maven ou Gradle. Vamos entender como resolver isso no próximo tópico.

Começando um projeto e preparando o uso do Python

Assim como eu não instalo Java via apt ou brew (uso Ubuntu em casa e macOS no serviço) pois uso SDKMan, faço o mesmo com Python por meio do Pyenv, assim posso trocar tranquilamente de versões.

OK, mas isso não resolve a questão dos pacotes instalados globalmente totalmente, embora eu possa trocar de versões Python, sendo que cada um tem seus pacotes instalados via pip, ainda sim posso sofrer de mutretas de libs de projetos distintos que usam a mesma versão Python e até os mesmos pacotes só que de versões diferentes, é aí que entra o Virtual Enviroments ou, como é popularmente conhecido, venv!

Quando você executa o comando python -m venv "./pasta-que-simula-ambiente-novo" na raiz do seu novo projeto, o módulo nativo venv (a partir da versão 3.3) cria a pasta com o nome gigante acima que propicia usarmos um ambiente local pro projeto totalmente livre de módulos instalados globalmente, contudo para ativarmos devemos executar o comando source pasta-que-simula-ambiente-novo/bin/activate. Olha como fica:

Se você instalar um pacote agora, ele só ficará disponível no seu projeto corrente. Caso queira sair do ambiente para voltar ao global, basta executar o comando deactivate. Assim você pode ir para outro e realizar o source novamente. Veja uma brincadeira que fiz no meu playground:

Para começar um projeto, acredito que o básico seja isso, embora exista o Pipenv, mas aí já é para outra postagem.

Organização do projeto

Com Maven ou Gradle temos uma estrutura bem definida do projeto, simplificando um clássico:

E como é com Python? Temos algo bem definido? Respondendo de bate-pronto: não tem. Quando afirmo isso é comparando com o mundo Java, onde a convenção é extremamente forte, o que não é verdade no Python. O que vejo é algo mais ou menos assim passando o exemplo:

Se você precisar de algo dentro de resources, por exemplo um arquivo JSON que servirá de insumo para um teste unitário, se fosse em Java, uma abordagem é obtê-lo via classpath, com Python precisamos acessá-lo via caminho absoluto/relativo ou por exemplo passando a responsabilidade para uma função, o que é melhor. Então dentro de support.py teríamos:

E no teste unitário podemos usar o recurso assim:

Dessa maneira pouco importa para o módulo do teste unitário onde o arquivo está, mas ele sabe que precisa de um your-sample.json, a localização em si, fica a cargo do support.py resolver.

Uma coisa que não comentei é sobre a pasta integration, lá teríamos os teste de integração. Com Maven usamos o plugin failsafe, só que os testes de integração ainda continuam em src/test/java, com a diferença do nome da classe terminar em IT em vez de Test (convenção padrão incluindo surefire).

Enfim, ainda assim é possível ver outros padrões por aí, como os que existem no PyBuilder, que é quase igual ao Maven. Por enquanto não o uso, mas testarei para ver os seus reais ganhos.

Testes unitários e mocks

Costumo usar o JUnit somente como framework de testes e AssertJ como biblioteca de asserções em Java e Kotlin. Com JavaScript gosto do casal Mocha e Chai, analogamente, embora Jasmine permita um bom trabalho também.

Com Python temos nativamente o unittest, que assim como o JUnit, é um framework de teste e de asserção, mas que ainda inclui API para Mock, estilo Mockito, então por enquanto não é necessário instalar libs externas. Pesquisando sobre o tema, decidi pular para um cara que em teoria é melhor, o tal do pytest. Antes de usá-lo, é necessário instalá-lo via pip. Um dos ganhos legais dele é que você pode criar um módulo e escrever as funções de teste diretamente, desde que comece com test_ (como alterar essa convenção aqui), já com unittest é diferente: você precisa criar uma classe que herde de unittest.TestCase.

Podemos misturar o uso de Mock disponível nativamente com pytest, sem problemas, mas também preferi usar o wrapper pytest-mock depois de ler uma postagem que sugere seu uso, além de ter um cheatsheet bem legal para uso de Mocks, leia também aqui. Tem algo que não decidi ainda: uma biblioteca focada em asserções. Sei que tem PyThuth, AssertPy, Grappa e outros, e parecem ser bem melhores do que a abordagem de asserções do unittest e do pytest, já que não é o foco deles, mas por enquanto continuarei com o nativo, por enquanto…

Qual IDE ou editor de texto

Meu caso é simples: já uso o IntelliJ IDEA Ultimate. É como se eu tivesse o PyCharm, só que via plugin oficial da JetBrains. Tenho vários ganhos análogos ao desenvolvimento com Java/Kotlin:

Enfim, é tranquilo e super prático, então além de ter suporte para linguagens JVM e JavaScript, Python também está incluído no pacote.

Já editor de texto, por uso prático, não posso recomendar pois não uso, mas se fosse, recomendaria o VS Code porque outros colegas do trabalho utilizam e acham super fácil.

Configurando log

Em Java normalmente temos log4j.xml ou logback.xml, sendo este último o padrão para qualquer projeto gerado via Spring Initializr. Resumindo é simples: com as libs corretas no projeto e o arquivo identificado no classpath, a configuração é aplicada. E aí é só usar uma instância do Logger para imprimir saídas em TRACE, DEBUG, INFO, WARN e/ou ERROR. Um monte de coisa é capturada ao se executar por exemplo log.info("salt"), exemplos:

  • Nome da classe
  • Método
  • Mensagem
  • Thread ID

Em Python temos isso também! Veja o exemplo abaixo que tirei das brincadeiras do Logging Cookbook:

A saída no console(STDOUT) fica assim:

Pitacos finais

Comparando com o mundo Java, você ficará impressionado: muita coisa é possível fazer com a lib standard do Python, sem a necessidade de instalar via pip outros. Sem contar a simplicidade da linguagem, é sensacional (com exceção para coroutines).

Antes de tomar a decisão, para aprender fiz os seguintes cursos:

E comprei os livros:

Mas sabe o que me fez aprender de verdade? Meter a mão na massa com um projeto. Eu posso até ter entendido bem a parte teórica e tals da coisa, mas a verdade máxima é: nada como a prática.

Ao som de The Legend of Zelda Ocarina Of Time, Title Theme.

--

--