Como otimizar seus scripts Python para melhor desempenho

Como Otimizar Seus Scripts Python Para Melhor Desempenho



Otimizar os scripts Python para melhor desempenho envolve identificar e resolver os gargalos em nosso código, tornando-o executado de forma mais rápida e eficiente. Python é uma linguagem de programação popular e poderosa usada em inúmeras aplicações atualmente, incluindo análise de dados, projetos de ML (aprendizado de máquina), desenvolvimento web e muito mais. A otimização do código Python é uma estratégia para melhorar a velocidade e a eficiência do programa do desenvolvedor ao realizar qualquer atividade usando menos linhas de código, menos memória ou recursos adicionais. Código grande e ineficiente pode retardar o programa, o que pode resultar em baixa satisfação do cliente e possíveis perdas financeiras, ou na necessidade de mais trabalho para corrigir e solucionar problemas.

É necessário ao realizar uma tarefa que exige o processamento de diversas ações ou dados. Portanto, trocar e aprimorar alguns blocos de código e funcionalidades ineficazes pode ter resultados surpreendentes como os seguintes:

  1. Aumente o desempenho do aplicativo
  2. Crie código legível e organizado
  3. Simplifique o monitoramento e a depuração de erros
  4. Conservar considerável poder computacional e assim por diante

Crie um perfil do seu código

Antes de começarmos a otimizar, é essencial identificar as partes do código do projeto que estão deixando-o lento. As técnicas para criação de perfil em Python incluem os pacotes cProfile e profile. Utilize essas ferramentas para avaliar a rapidez com que certas funções e linhas de código são executadas. O módulo cProfile produz um relatório que detalha quanto tempo cada função de script leva para ser executada. Este relatório pode nos ajudar a encontrar quaisquer funções que estejam funcionando lentamente para que possamos melhorá-las.







Fragmento de código:



importar cPerfil como CP
definição calcularSoma ( número de entrada ) :
soma_de_números_de_entrada = 0
enquanto número de entrada > 0 :
soma_de_números_de_entrada + = número de entrada % 10
número de entrada // = 10
imprimir ( 'A soma de todos os dígitos no número de entrada é: 'sum_of_input_numbers'' )
retornar soma_de_números_de_entrada
definição função_principal ( ) :
CP. correr ( 'calcularSoma(9876543789)' )
se __nome__ == '__principal__' :
função_principal ( )

O programa faz um total de cinco chamadas de função, conforme visto na primeira linha da saída. Os detalhes de cada chamada de função são mostrados nas linhas a seguir, incluindo o número de vezes que a função foi invocada, a duração total do tempo na função, a duração do tempo por chamada e a quantidade total de tempo na função (incluindo todas as funções que ele é chamado).



Além disso, o programa imprime um relatório na tela de prompt que mostra que o programa conclui o tempo de execução de todas as suas tarefas em 0,000 segundos. Isso mostra o quão rápido o programa é.





Escolha a estrutura de dados correta

As características de desempenho dependem da estrutura de dados. Em particular, os dicionários são mais rápidos para pesquisas do que as listas relativas ao armazenamento de uso geral. Selecione a estrutura de dados mais adequada para as operações que realizaremos com seus dados, caso você as conheça. O exemplo a seguir investiga a eficácia de diferentes estruturas de dados para um processo idêntico para determinar se um elemento na estrutura de dados está presente.



Avaliamos o tempo necessário para verificar se um elemento está presente em cada estrutura de dados – uma lista, um conjunto e um dicionário – e os comparamos.

OptimizeDataType.py:

importar Tempoi como tt
importar aleatório como rndobj
# Gera uma lista de inteiros
lista_de_dados_aleatórios = [ rndobj. aleatório ( 1 , 10.000 ) para _ em faixa ( 10.000 ) ]
# Crie um conjunto a partir dos mesmos dados
conjunto_de_dados_aleatórios = definir ( lista_de_dados_aleatórios )

# Cria um dicionário com os mesmos dados das chaves
obj_DataDictionary = { num: Nenhum para num em lista_de_dados_aleatórios }

# Elemento a ser pesquisado (existe nos dados)
número_aleatório_para_encontrar = rndobj. escolha ( lista_de_dados_aleatórios )

# Meça o tempo para verificar a adesão a uma lista
horário_da_lista = tt. Tempoi ( lambda : número_aleatório_para_encontrar em lista_de_dados_aleatórios , número = 1000 )

# Meça o tempo para verificar a adesão a um conjunto
definir tempo = tt. Tempoi ( lambda : número_aleatório_para_encontrar em conjunto_de_dados_aleatórios , número = 1000 )

# Meça o tempo para verificar a adesão em um dicionário
dict_time = tt. Tempoi ( lambda : número_aleatório_para_encontrar em obj_DataDictionary , número = 1000 )

imprimir ( f 'Tempo de verificação de associação à lista: {list_time:.6f} segundos' )
imprimir ( f 'Definir tempo de verificação de adesão: {set_time:.6f} segundos' )
imprimir ( f 'Tempo de verificação de associação ao dicionário: {dict_time:.6f} segundos' )

Este código compara o desempenho de listas, conjuntos e dicionários ao fazer verificações de associação. Em geral, conjuntos e dicionários são substancialmente mais rápidos do que listas para testes de adesão porque usam pesquisas baseadas em hash, portanto, têm uma complexidade de tempo média de O(1). As listas, por outro lado, devem fazer pesquisas lineares que resultam em testes de pertinência com complexidade de tempo O(n).

  Uma captura de tela de um computador Descrição gerada automaticamente

Use as funções integradas em vez de loops

Numerosas funções ou métodos integrados em Python podem ser usados ​​para realizar tarefas típicas como filtragem, classificação e mapeamento. Usar essas rotinas em vez de criar loops ajuda a acelerar o código porque elas são frequentemente otimizadas para desempenho.

Vamos construir alguns exemplos de código para comparar o desempenho da criação de loops personalizados utilizando as funções integradas para trabalhos típicos (como map(), filter() e sorted()). Avaliaremos o desempenho dos vários métodos de mapeamento, filtragem e classificação.

BuiltInFunctions.py:

importar Tempoi como tt
# Exemplo de lista de numbers_list
lista_de_números = lista ( faixa ( 1 , 10.000 ) )

# Função para elevar number_list ao quadrado usando um loop
definição quadrado_usando_loop ( lista_de_números ) :
resultado_quadrado = [ ]
para num em lista_de_números:
resultado_quadrado. acrescentar ( num ** 2 )
retornar resultado_quadrado
# Função para filtrar números pares_lista usando um loop
definição filter_even_using_loop ( lista_de_números ) :
resultado_filtro = [ ]
para num em lista_de_números:
se num % 2 == 0 :
resultado_filtro. acrescentar ( num )
retornar resultado_filtro
# Função para ordenar numbers_list usando um loop
definição sort_using_loop ( lista_de_números ) :
retornar classificado ( lista_de_números )
# Meça o tempo para elevar number_list ao quadrado usando map()
hora_mapa = tt. Tempoi ( lambda : lista ( mapa ( lambda x:x** 2 , lista_de_números ) ) , número = 1000 )
# Meça o tempo para filtrar números pares_list usando filter()
tempo_filtro = tt. Tempoi ( lambda : lista ( filtro ( lambda x: x% 2 == 0 , lista_de_números ) ) , número = 1000 )
# Meça o tempo para classificar numbers_list usando sorted()
hora_classificada = tt. Tempoi ( lambda : classificado ( lista_de_números ) , número = 1000 )
# Meça o tempo para elevar números_list ao quadrado usando um loop
loop_map_time = tt. Tempoi ( lambda : square_using_loop ( lista_de_números ) , número = 1000 )
# Meça o tempo para filtrar números pares_lista usando um loop
loop_filter_time = tt. Tempoi ( lambda : filter_even_using_loop ( lista_de_números ) , número = 1000 )
# Meça o tempo para classificar numbers_list usando um loop
loop_sorted_time = tt. Tempoi ( lambda : sort_using_loop ( lista_de_números ) , número = 1000 )
imprimir ( 'Lista de números contém 10.000 elementos' )
imprimir ( f 'Mapa() Tempo: {map_time:.6f} segundos' )
imprimir ( f 'Tempo do filtro(): {filter_time:.6f} segundos' )
imprimir ( f 'Tempo classificado(): {tempo_classificado:.6f} segundos' )
imprimir ( f 'Tempo de loop (mapa): {loop_map_time:.6f} segundos' )
imprimir ( f 'Tempo de loop (filtro): {loop_filter_time:.6f} segundos' )
imprimir ( f 'Tempo de loop (classificado): {loop_sorted_time:.6f} segundos' )

Provavelmente observaremos que as funções integradas (map(), filter() e sorted()) são mais rápidas do que os loops personalizados para essas tarefas comuns. As funções integradas em Python oferecem uma abordagem mais concisa e compreensível para realizar essas tarefas e são altamente otimizadas para desempenho.

Otimize os Loops

Se for necessário escrever os loops, existem algumas técnicas que podemos usar para acelerá-los. Geralmente, o loop range() é mais rápido do que iterar para trás. Isso ocorre porque range() gera um iterador sem inverter a lista, o que pode ser uma operação cara para listas longas. Além disso, como range() não cria uma nova lista na memória, ele usa menos memória.

OptimizeLoop.py:

importar Tempoi como tt
# Exemplo de lista de numbers_list
lista_de_números = lista ( faixa ( 1 , 100.000 ) )
#Função para iterar a lista na ordem inversa
definição loop_reverse_iteration ( ) :
resultado_reverso = [ ]
para j em faixa ( apenas ( lista_de_números ) - 1 , - 1 , - 1 ) :
resultado_reverso. acrescentar ( lista_de_números [ j ] )
retornar resultado_reverso
#Função para iterar na lista usando range()
definição loop_range_iteration ( ) :
intervalo_resultados = [ ]
para k em faixa ( apenas ( lista_de_números ) ) :
intervalo_resultado. acrescentar ( lista_de_números [ k ] )
retornar intervalo_resultados
# Meça o tempo necessário para realizar a iteração reversa
tempo_reverso = tt. Tempoi ( loop_reverse_iteration , número = 1000 )
# Meça o tempo necessário para realizar a iteração do intervalo
intervalo_tempo = tt. Tempoi ( loop_range_iteration , número = 1000 )
imprimir ( 'A lista de números contém 100.000 registros' )
imprimir ( f 'Tempo de iteração reversa: {reverse_time:.6f} segundos' )
imprimir ( f 'Tempo de iteração do intervalo: {range_time:.6f} segundos' )

Evite chamadas de função desnecessárias

Há alguma sobrecarga toda vez que uma função é chamada. O código é executado mais rapidamente se chamadas de função desnecessárias forem evitadas. Por exemplo, em vez de executar repetidamente uma função que calcula um valor, tente armazenar o resultado do cálculo em uma variável e utilizá-la.

Ferramentas para criação de perfil

Para saber mais sobre o desempenho do seu código, além da criação de perfil integrada, podemos utilizar pacotes de criação de perfil externos como cProfile, Pyflame ou SnakeViz.

Resultados de cache

Se nosso código precisar realizar cálculos caros, podemos considerar armazenar os resultados em cache para economizar tempo.

Refatoração de código

Refatorar o código para torná-lo mais fácil de ler e manter às vezes é uma parte necessária para otimizá-lo. Um programa mais rápido também pode ser mais limpo.

Use a compilação Just-in-Time (JIT)

Bibliotecas como PyPy ou Numba podem fornecer uma compilação JIT que pode acelerar significativamente certos tipos de código Python.

Atualizar Python

Certifique-se de estar usando a versão mais recente do Python, pois as versões mais recentes geralmente incluem melhorias de desempenho.

Paralelismo e simultaneidade

Para processos que podem ser paralelizados, investigue as técnicas paralelas e de sincronização, como multiprocessamento, threading ou assíncio.

Lembre-se de que o benchmarking e o perfil devem ser os principais impulsionadores da otimização. Concentre-se em melhorar as áreas do nosso código que têm os efeitos mais significativos no desempenho e teste constantemente suas melhorias para garantir que elas tenham os efeitos desejados sem introduzir mais defeitos.

Conclusão

Concluindo, a otimização do código Python é crucial para melhorar o desempenho e a eficácia dos recursos. Os desenvolvedores podem aumentar muito a velocidade de execução e a capacidade de resposta de seus aplicativos Python usando várias técnicas, como selecionar as estruturas de dados apropriadas, aproveitar as funções integradas, reduzir os loops extras e gerenciar a memória de maneira eficaz. O benchmarking e a criação de perfil contínuos devem direcionar os esforços de otimização, garantindo que os avanços do código correspondam aos requisitos de desempenho do mundo real. Para garantir o sucesso do projeto a longo prazo e diminuir a chance de introdução de novos problemas, a otimização do código deve ser constantemente equilibrada com os objetivos de legibilidade e manutenção do código.