Em geral, as pessoas usam Python porque é conveniente e fácil de programar, não porque seja rápido. A infinidade de bibliotecas de terceiros e a amplitude do suporte da indústria para Python compensam fortemente o fato de ele não ter o desempenho bruto de Java ou C. A velocidade de desenvolvimento tem precedência sobre a velocidade de execução.

Mas em muitos casos, não precisa ser uma proposição ou/ou. Devidamente otimizados, os aplicativos Python podem ser executados com uma velocidade surpreendente – talvez não tão rápido quanto Java ou C, mas rápido o suficiente para aplicativos da web, análise de dados, ferramentas de gerenciamento e automação e muitos outros propósitos. Com as otimizações certas, você talvez nem perceba a compensação entre o desempenho do aplicativo e a produtividade do desenvolvedor.

Otimizar o desempenho do Python não se resume a nenhum fator. Em vez disso, trata-se de aplicar todas as melhores práticas disponíveis e escolher aquelas que melhor se adaptam ao cenário em questão. (O pessoal do Dropbox tem um dos exemplos mais impressionantes do poder das otimizações Python.)

Neste artigo, discutirei 10 otimizações comuns do Python. Algumas são medidas imediatas que exigem pouco mais do que trocar um item por outro (como mudar o interpretador Python); outros proporcionam resultados maiores, mas também exigem um trabalho mais detalhado.

10 maneiras de fazer programas Python rodarem mais rápido

  • Medir, medir, medir
  • Memoize (cache) dados usados ​​repetidamente
  • Mova matemática para NumPy
  • Mova matemática para Numba
  • Use uma biblioteca C
  • Converter para Cython
  • Vá em paralelo com o multiprocessamento
  • Saiba o que suas bibliotecas estão fazendo
  • Saiba o que sua plataforma está fazendo
  • Execute com PyPy

Medir, medir, medir

Você não pode perder o que não mede, como diz o velho ditado. Da mesma forma, você não pode descobrir por que um determinado aplicativo Python funciona de maneira abaixo do ideal sem descobrir onde reside a lentidão.

Comece com um perfil simples por meio do recurso integrado do Python cProfile módulo e mude para um criador de perfil mais poderoso se precisar de maior precisão ou maior profundidade de insights. Freqüentemente, os insights obtidos pela inspeção básica em nível de função de um aplicativo fornecem uma perspectiva mais do que suficiente. (Você pode extrair dados de perfil para uma única função por meio do profilehooks módulo.)

Por que uma parte específica do aplicativo é muito lenta e como corrigi-la pode exigir mais pesquisas. O objetivo é restringir o foco, estabelecer uma linha de base com números concretos e testar em vários cenários de uso e implantação sempre que possível. Não otimize prematuramente. Adivinhar não leva você a lugar nenhum.

O exemplo do Dropbox (link acima) mostra como o perfil é útil. “Foi a medição que nos disse que o escape do HTML era lento para começar”, escreveram os desenvolvedores, “e sem medir o desempenho, nunca teríamos imaginado que a interpolação de strings era tão lenta”.

Memoize (cache) dados usados ​​repetidamente

Nunca trabalhe mil vezes quando você pode fazê-lo uma vez e salvar os resultados. Se você tiver uma função chamada com frequência que retorna resultados previsíveis, o Python oferece opções para armazenar os resultados em cache na memória. As chamadas subsequentes que retornarem o mesmo resultado retornarão quase imediatamente.

Vários exemplos mostram como fazer isso; minha memoização favorita é quase tão mínima quanto possível. Mas o Python tem essa funcionalidade integrada. Uma das bibliotecas nativas do Python, functoolstem o @functools.lru_cache decorador, que armazena em cache o n chamadas mais recentes para uma função. Isso é útil quando o valor que você está armazenando em cache muda, mas é relativamente estático dentro de um determinado período de tempo. Uma lista dos itens usados ​​mais recentemente ao longo do dia seria um bom exemplo.

Observe que se você tiver certeza de que a variedade de chamadas para a função permanecerá dentro de um limite razoável (por exemplo, 100 resultados diferentes em cache), você poderá usar @functools.cache, que tem melhor desempenho.

Mova matemática para NumPy

Se você estiver fazendo matemática baseada em matrizes ou arrays e não quiser que o interpretador Python atrapalhe, use NumPy. Baseando-se em bibliotecas C para o trabalho pesado, o NumPy oferece processamento de array mais rápido do que o Python nativo. Ele também armazena dados numéricos com mais eficiência do que as estruturas de dados integradas do Python.

Outra vantagem do NumPy é o uso mais eficiente da memória para objetos grandes, como listas com milhões de itens. Em média, objetos grandes como o NumPy ocupam cerca de um quarto da memória necessária se fossem expressos em Python convencional. Observe que ajuda começar com a estrutura de dados correta para um trabalho – o que é uma otimização em si.

Reescrever algoritmos Python para usar NumPy dá algum trabalho, pois os objetos array precisam ser declarados usando a sintaxe do NumPy. Além disso, os maiores ganhos de velocidade ocorrem por meio do uso de técnicas de “transmissão” específicas do NumPy, onde uma função ou comportamento é aplicado em um array. Reserve um tempo para se aprofundar na documentação do NumPy para descobrir quais funções estão disponíveis e como usá-las bem.

Além disso, embora o NumPy seja adequado para acelerar a matemática baseada em matrizes ou arrays, ele não fornece uma aceleração útil para a matemática realizada fora dos arrays ou matrizes NumPy. A matemática que envolve objetos Python convencionais não terá aceleração.

Mova matemática para Numba

Outra biblioteca poderosa para acelerar operações matemáticas é a Numba. Escreva algum código Python para manipulação numérica e envolva-o com o compilador JIT (just-in-time) do Numba, e o código resultante será executado na velocidade nativa da máquina. Numba não apenas fornece acelerações alimentadas por GPU (CUDA e ROC), mas também possui um “nopython”Modo que tenta maximizar o desempenho não dependendo do interpretador Python sempre que possível.

O Numba também trabalha lado a lado com o NumPy, para que você possa obter o melhor dos dois mundos – NumPy para todas as operações que pode resolver e Numba para todo o resto.

Use uma biblioteca C

O uso de bibliotecas escritas em C pelo NumPy é uma boa estratégia para emular. Se já houver uma biblioteca C que faça o que você precisa, o Python e seu ecossistema oferecem diversas opções para se conectar à biblioteca e aproveitar sua velocidade.

A maneira mais comum de fazer isso é a biblioteca ctypes do Python. Porque ctypes é amplamente compatível com outros aplicativos Python (e tempos de execução), é o melhor lugar para começar, mas está longe de ser o único jogo disponível. O projeto CFFI fornece uma interface mais elegante para C. Cython (veja abaixo) também pode ser usado para escrever suas próprias bibliotecas C ou agrupar bibliotecas externas existentes, embora ao custo de ter que aprender a marcação do Cython.

Uma advertência aqui: você obterá os melhores resultados minimizando o número de viagens de ida e volta entre C e Python. Cada vez que você passa dados entre eles, isso prejudica o desempenho. Se você tiver a opção entre chamar uma biblioteca C em um loop estreito ou passar uma estrutura de dados inteira para a biblioteca C e executar o processamento em loop, escolha a segunda opção. Você fará menos viagens de ida e volta entre domínios.

Converter para Cython

Se você quer velocidade, use C, não Python. Mas para Pythonistas, escrever código C traz uma série de distrações – aprender a sintaxe de C, discutir o conjunto de ferramentas C (o que há de errado com meus arquivos de cabeçalho agora?), e assim por diante.

Cython permite que usuários de Python acessem convenientemente a velocidade de C. O código Python existente pode ser convertido para C de forma incremental – primeiro compilando o código em C com Cython e, em seguida, adicionando anotações de tipo para obter mais velocidade.

Cython não é uma varinha mágica. O código convertido como está para Cython, sem anotações de tipo, geralmente não é executado mais do que 15 a 50 por cento mais rápido. Isso ocorre porque a maioria das otimizações nesse nível se concentra na redução da sobrecarga do interpretador Python. Os maiores ganhos ocorrem quando suas variáveis ​​podem ser anotadas como tipos C – por exemplo, um número inteiro de 64 bits em nível de máquina em vez do número inteiro do Python. int tipo. As acelerações resultantes podem ser ordens de magnitude mais rápidas.

O código vinculado à CPU é o que mais se beneficia do Cython. Se você criou um perfil (você ter perfilado, não é?) e descobri que certas partes do seu código usam a grande maioria do tempo da CPU, esses são excelentes candidatos para conversão Cython. O código vinculado à E/S, como operações de rede de longa duração, terá pouco ou nenhum benefício do Cython.

Assim como acontece com o uso de bibliotecas C, outra dica importante para melhorar o desempenho é manter o número de viagens de ida e volta ao Cython no mínimo. Não escreva um loop que chame uma função “Cythonizada” repetidamente; implemente o loop no Cython e passe os dados todos de uma vez.

Vá em paralelo com o multiprocessamento

Aplicativos Python tradicionais – aqueles implementados em CPython – executam apenas um único thread por vez, para evitar os problemas de estado que surgem ao usar vários threads. Este é o infame Global Interpreter Lock (GIL). Existem boas razões para a sua existência, mas isso não a torna menos teimosa.

Um aplicativo CPython pode ser multithread, mas por causa do GIL, o CPython não permite que esses threads sejam executados em paralelo em vários núcleos. O GIL tornou-se dramaticamente mais eficiente ao longo do tempo e há trabalho em andamento para removê-lo totalmente, mas por enquanto a questão central permanece.

Uma solução comum é o módulo de multiprocessamento, que executa múltiplas instâncias do interpretador Python em núcleos separados. O estado pode ser compartilhado por meio de memória compartilhada ou processos de servidor, e os dados podem ser transmitidos entre instâncias de processo por meio de filas ou canais.

Você ainda precisa gerenciar o estado manualmente entre os processos. Além disso, não há uma pequena sobrecarga envolvida no início de múltiplas instâncias do Python e na passagem de objetos entre elas. Mas para processos de longa execução que se beneficiam do paralelismo entre núcleos, a biblioteca de multiprocessamento é útil.

Além disso, módulos e pacotes Python que usam bibliotecas C (como NumPy ou Cython) são capazes de evitar totalmente o GIL. Essa é outra razão pela qual eles são recomendados para aumentar a velocidade.

Saiba o que suas bibliotecas estão fazendo

Como é conveniente simplesmente digitar include foobar e aproveite o trabalho de inúmeros outros programadores! Mas você precisa estar ciente de que bibliotecas de terceiros podem alterar o desempenho da sua aplicação, nem sempre para melhor.

Às vezes, isso se manifesta de maneiras óbvias, como quando um módulo de uma biblioteca específica constitui um gargalo. (Novamente, a criação de perfil ajudará.) Às vezes é menos óbvio. Por exemplo, considere o Pyglet, uma biblioteca útil para criar aplicativos gráficos em janelas. O Pyglet ativa automaticamente um modo de depuração, que afeta drasticamente o desempenho até que seja explicitamente desativado. Talvez você nunca perceba isso, a menos que leia a documentação da biblioteca; portanto, quando começar a trabalhar com uma nova biblioteca, leia e esteja informado.

Saiba o que sua plataforma está fazendo

O Python é executado em várias plataformas, mas isso não significa que as peculiaridades de cada sistema operacional – Windows, Linux, macOS – sejam totalmente abstraídas no Python. Na maioria das vezes, vale a pena estar ciente das especificidades da plataforma, como convenções de nomenclatura de caminhos, para as quais existem funções auxiliares. O módulo pathlib, por exemplo, abstrai convenções de caminho específicas da plataforma. O manuseio do console também varia muito entre o Windows e outros sistemas operacionais; daí a popularidade de abstrair bibliotecas como ricas.

Em algumas plataformas, certos recursos não são suportados e isso pode afetar o modo como você escreve Python. O Windows, por exemplo, não possui o conceito de bifurcação de processos; portanto, algumas funcionalidades de multiprocessamento funcionam de maneira diferente.

Finalmente, a maneira como o Python é instalado e executado na plataforma também é importante. No Linux, por exemplo, pip normalmente é instalado separadamente do próprio Python; no Windows, é instalado automaticamente com Python.

Execute com PyPy

CPython, a implementação de Python mais comumente usada, prioriza a compatibilidade em vez da velocidade bruta. Para programadores que desejam colocar a velocidade em primeiro lugar, existe o PyPy, uma implementação Python equipada com um compilador JIT para acelerar a execução do código.

Como o PyPy foi projetado como um substituto imediato para o CPython, é uma das maneiras mais simples de obter um rápido aumento de desempenho. Muitos aplicativos Python comuns serão executados no PyPy exatamente como estão. Geralmente, quanto mais o aplicativo depende do Python “vanilla”, maior a probabilidade de ele ser executado no PyPy sem modificação.

No entanto, tirar o melhor proveito do PyPy pode exigir testes e estudo. Você descobrirá que os aplicativos de longa execução obtêm os maiores ganhos de desempenho do PyPy, porque o compilador analisa a execução ao longo do tempo para determinar como acelerar as coisas. Para scripts curtos que apenas são executados e encerrados, provavelmente será melhor usar o CPython, pois os ganhos de desempenho não serão suficientes para superar a sobrecarga do JIT.

Observe que o suporte do PyPy para Python tende a ficar atrasado em relação às versões mais atuais da linguagem. Quando o Python 3.12 era atual, o PyPy era compatível apenas até a versão 3.10. Além disso, aplicativos Python que usam ctypes nem sempre pode se comportar como esperado. Se você estiver escrevendo algo que possa ser executado tanto no PyPy quanto no CPython, pode fazer sentido lidar com os casos de uso separadamente para cada intérprete.