Come ottimizzare i tuoi script Python per prestazioni migliori

Come Ottimizzare I Tuoi Script Python Per Prestazioni Migliori



L'ottimizzazione degli script Python per prestazioni migliori implica l'identificazione e la risoluzione dei colli di bottiglia nel nostro codice, rendendolo più veloce ed efficiente. Python è un linguaggio di programmazione popolare e potente che oggigiorno viene utilizzato in numerose applicazioni tra cui analisi dei dati, progetti ML (apprendimento automatico), sviluppo web e molto altro. L'ottimizzazione del codice Python è una strategia per migliorare la velocità e l'efficienza del programma di sviluppo durante l'esecuzione di qualsiasi attività utilizzando meno righe di codice, meno memoria o risorse aggiuntive. Un codice grande e inefficiente può rallentare il programma, con conseguente scarsa soddisfazione del cliente e potenziale perdita finanziaria, o la necessità di ulteriore lavoro per correggere e risolvere i problemi.

È necessario durante l'esecuzione di un'attività che richiede l'elaborazione di diverse azioni o dati. Pertanto, la sostituzione e il miglioramento di alcuni blocchi di codice e funzionalità inefficaci può avere risultati sorprendenti come i seguenti:

  1. Aumenta le prestazioni dell'applicazione
  2. Crea codice leggibile e organizzato
  3. Semplifica il monitoraggio e il debug degli errori
  4. Conservare una notevole potenza di calcolo e così via

Profila il tuo codice

Prima di iniziare a ottimizzare, è essenziale identificare le parti del codice del progetto che lo rallentano. Le tecniche per la profilazione in Python includono i pacchetti cProfile e profile. Utilizza tali strumenti per valutare la velocità con cui vengono eseguite determinate funzioni e righe di codice. Il modulo cProfile produce un report che descrive in dettaglio il tempo necessario per l'esecuzione di ciascuna funzione dello script. Questo rapporto può aiutarci a trovare eventuali funzioni che funzionano lentamente in modo da poterle migliorare.







Frammento di codice:



importare cProfilo COME cP
def calcolaSum ( inputNumero ) :
somma_di_numeri_input = 0
Mentre inputNumero > 0 :
somma_di_numeri_input + = inputNumero % 10
inputNumero // = 10
stampa ( 'La somma di tutte le cifre nel numero di input è: 'sum_of_input_numbers'' )
ritorno somma_di_numeri_input
def main_funz ( ) :
cP. correre ( 'calcolaSomma(9876543789)' )
Se __nome__ == '__principale__' :
main_funz ( )

Il programma effettua un totale di cinque chiamate di funzione come mostrato nella prima riga dell'output. I dettagli di ciascuna chiamata di funzione sono mostrati nelle righe seguenti, incluso il numero di volte in cui la funzione è stata invocata, la durata complessiva del tempo nella funzione, la durata del tempo per chiamata e la quantità complessiva di tempo nella funzione (incluso tutte le funzioni che viene chiamato).



Inoltre, il programma stampa un rapporto sulla schermata del prompt che mostra che il programma completa il tempo di esecuzione di tutte le sue attività entro 0,000 secondi. Questo mostra quanto è veloce il programma.





Scegli la giusta struttura dati

Le caratteristiche prestazionali dipendono dalla struttura dei dati. In particolare, i dizionari sono più rapidi nelle ricerche rispetto agli elenchi riguardanti l'archiviazione generica. Seleziona la struttura dati più adatta alle operazioni che effettueremo sui tuoi dati se le conosci. L'esempio seguente analizza l'efficacia di diverse strutture dati per un processo identico per determinare se un elemento nella struttura dati è presente.



Valutiamo il tempo necessario per verificare se un elemento è presente in ciascuna struttura dati (una lista, un insieme e un dizionario) e li confrontiamo.

OptimizeDataType.py:

importare Timei COME tt
importare casuale COME rndobj
# Genera un elenco di numeri interi
lista_dati_casuali = [ rndobj. randint ( 1 , 10000 ) per _ In allineare ( 10000 ) ]
# Crea un set dagli stessi dati
set_dati_casuali = impostato ( lista_dati_casuali )

# Crea un dizionario con gli stessi dati delle chiavi
obj_DataDictionary = { nessuno: Nessuno per nessuno In lista_dati_casuali }

# Elemento da cercare (esiste nei dati)
numero_casuale_da_trovare = rndobj. scelta ( lista_dati_casuali )

# Misura il tempo per verificare l'appartenenza a un elenco
lista_ora = tt. Timei ( lambda : numero_casuale_da_trovare In lista_dati_casuali , numero = 1000 )

# Misura il tempo per verificare l'appartenenza a un set
tempo impostato = tt. Timei ( lambda : numero_casuale_da_trovare In set_dati_casuali , numero = 1000 )

# Misura il tempo per verificare l'appartenenza a un dizionario
dict_time = tt. Timei ( lambda : numero_casuale_da_trovare In obj_DataDictionary , numero = 1000 )

stampa ( F 'Tempo di controllo appartenenza all'elenco: {list_time:.6f} secondi' )
stampa ( F 'Imposta tempo controllo iscrizione: {set_time:.6f} secondi' )
stampa ( F 'Tempo di controllo dell'appartenenza al dizionario: {dict_time:.6f} secondi' )

Questo codice confronta le prestazioni di elenchi, set e dizionari durante i controlli di appartenenza. In generale, insiemi e dizionari sono sostanzialmente più veloci degli elenchi per i test di appartenenza perché utilizzano ricerche basate su hash, quindi hanno una complessità temporale media di O(1). Le liste, d'altro canto, devono effettuare ricerche lineari che risultano in test di appartenenza con complessità temporale O(n).

  Schermata di una descrizione del computer generata automaticamente

Usa le funzioni integrate invece dei loop

Numerose funzioni o metodi integrati in Python possono essere utilizzati per eseguire attività tipiche come filtraggio, ordinamento e mappatura. Usare queste routine invece di creare i propri cicli aiuta ad accelerare il codice perché sono spesso ottimizzate per le prestazioni.

Costruiamo del codice di esempio per confrontare le prestazioni della creazione di loop personalizzati utilizzando le funzioni integrate per lavori tipici (come map(), filter() e sorted()). Valuteremo il rendimento dei vari metodi di mappatura, filtraggio e ordinamento.

BuiltInFunctions.py:

importare Timei COME tt
# Elenco di esempio di elenco_numeri
lista_numeri = elenco ( allineare ( 1 , 10000 ) )

# Funzione per quadrare elenco_numeri utilizzando un ciclo
def quadrato_usando_loop ( lista_numeri ) :
quadrato_risultato = [ ]
per nessuno In lista_numeri:
quadrato_risultato. aggiungere ( nessuno ** 2 )
ritorno quadrato_risultato
# Funzione per filtrare anche i numeri_elenco utilizzando un ciclo
def filter_even_using_loop ( lista_numeri ) :
filtro_risultato = [ ]
per nessuno In lista_numeri:
Se nessuno % 2 == 0 :
filtro_risultato. aggiungere ( nessuno )
ritorno filtro_risultato
# Funzione per ordinare elenco_numeri utilizzando un ciclo
def sort_using_loop ( lista_numeri ) :
ritorno smistato ( lista_numeri )
# Misura il tempo necessario per quadrare number_list utilizzando map()
map_time = tt. Timei ( lambda : elenco ( carta geografica ( lambda x:x** 2 , lista_numeri ) ) , numero = 1000 )
# Misura il tempo necessario per filtrare anche number_list utilizzando filter()
filter_time = tt. Timei ( lambda : elenco ( filtro ( lambda x:x% 2 == 0 , lista_numeri ) ) , numero = 1000 )
# Misura il tempo necessario per ordinare i numeri_lista utilizzando sorted()
sorted_time = tt. Timei ( lambda : smistato ( lista_numeri ) , numero = 1000 )
# Misura il tempo necessario per quadrare numeri_elenco utilizzando un ciclo
loop_map_time = tt. Timei ( lambda : quadrato_usando_loop ( lista_numeri ) , numero = 1000 )
# Misura il tempo necessario per filtrare anche i numeri_elenco utilizzando un ciclo
loop_filter_time = tt. Timei ( lambda : filter_even_using_loop ( lista_numeri ) , numero = 1000 )
# Misura il tempo necessario per ordinare elenco_numeri utilizzando un ciclo
loop_sorted_time = tt. Timei ( lambda : sort_using_loop ( lista_numeri ) , numero = 1000 )
stampa ( 'L'elenco dei numeri contiene 10000 elementi' )
stampa ( F 'Tempo mappa(): {map_time:.6f} secondi' )
stampa ( F 'Tempo filtro(): {filter_time:.6f} secondi' )
stampa ( F 'Ordinato() Tempo: {sorted_time:.6f} secondi' )
stampa ( F 'Tempo del loop (mappa): {loop_map_time:.6f} secondi' )
stampa ( F 'Tempo di loop (filtro): {loop_filter_time:.6f} secondi' )
stampa ( F 'Tempo del ciclo (ordinato): {loop_sorted_time:.6f} secondi' )

Probabilmente osserveremo che le funzioni integrate (map(), filter() e sorted()) sono più veloci dei cicli personalizzati per queste attività comuni. Le funzioni integrate in Python offrono un approccio più conciso e comprensibile per svolgere queste attività e sono altamente ottimizzate per le prestazioni.

Ottimizza i loop

Se è necessario scrivere i loop, ci sono alcune tecniche che possiamo adottare per velocizzarli. In genere, il ciclo range() è più veloce dell'iterazione all'indietro. Questo perché range() genera un iteratore senza invertire l'elenco, il che può essere un'operazione costosa per elenchi lunghi. Inoltre, poiché range() non crea un nuovo elenco in memoria, utilizza meno memoria.

OptimizeLoop.py:

importare Timei COME tt
# Elenco di esempio di elenco_numeri
lista_numeri = elenco ( allineare ( 1 , 100000 ) )
# Funzione per scorrere l'elenco in ordine inverso
def loop_iterazione_inversa ( ) :
risultato_inverso = [ ]
per J In allineare ( soltanto ( lista_numeri ) - 1 , - 1 , - 1 ) :
risultato_inverso. aggiungere ( lista_numeri [ J ] )
ritorno risultato_inverso
# Funzione per scorrere l'elenco utilizzando range()
def loop_range_iterazione ( ) :
intervallo_risultato = [ ]
per K In allineare ( soltanto ( lista_numeri ) ) :
intervallo_risultato. aggiungere ( lista_numeri [ K ] )
ritorno intervallo_risultato
# Misura il tempo necessario per eseguire l'iterazione inversa
reverse_time = tt. Timei ( loop_iterazione_inversa , numero = 1000 )
# Misura il tempo necessario per eseguire l'iterazione dell'intervallo
intervallo_tempo = tt. Timei ( loop_range_iterazione , numero = 1000 )
stampa ( 'L'elenco dei numeri contiene 100000 record' )
stampa ( F 'Tempo di iterazione inversa: {reverse_time:.6f} secondi' )
stampa ( F 'Tempo di iterazione dell'intervallo: {range_time:.6f} secondi' )

Evita chiamate di funzioni non necessarie

C'è un sovraccarico ogni volta che viene chiamata una funzione. Il codice viene eseguito più rapidamente se si evitano chiamate di funzioni non necessarie. Ad esempio, invece di eseguire ripetutamente una funzione che calcola un valore, prova a memorizzare il risultato del calcolo in una variabile e ad utilizzarlo.

Strumenti per la profilazione

Per saperne di più sulle prestazioni del tuo codice, oltre alla profilazione integrata, possiamo utilizzare i pacchetti di profilazione esterna come cProfile, Pyflame o SnakeViz.

Risultati nella cache

Se il nostro codice deve eseguire calcoli costosi, potremmo prendere in considerazione la memorizzazione nella cache dei risultati per risparmiare tempo.

Refactoring del codice

Il refactoring del codice per renderlo più facile da leggere e mantenere è talvolta una parte necessaria della sua ottimizzazione. Un programma più veloce può anche essere più pulito.

Utilizzare la compilazione Just-in-Time (JIT)

Librerie come PyPy o Numba possono fornire una compilazione JIT che può velocizzare significativamente alcuni tipi di codice Python.

Aggiorna Python

Assicurati di utilizzare la versione più recente di Python poiché le versioni più recenti spesso includono miglioramenti delle prestazioni.

Parallelismo e concorrenza

Per i processi che possono essere parallelizzati, esaminare le tecniche parallele e di sincronizzazione come multiprocessing, threading o asyncio.

Ricorda che il benchmarking e la profilazione dovrebbero essere i principali motori dell'ottimizzazione. Concentrati sul miglioramento delle aree del nostro codice che hanno gli effetti più significativi sulle prestazioni e testa costantemente i tuoi miglioramenti per assicurarti che abbiano gli effetti desiderati senza introdurre ulteriori difetti.

Conclusione

In conclusione, l'ottimizzazione del codice Python è fondamentale per migliorare le prestazioni e l'efficacia delle risorse. Gli sviluppatori possono aumentare notevolmente la velocità di esecuzione e la reattività delle loro applicazioni Python utilizzando varie tecniche come la selezione delle strutture dati appropriate, lo sfruttamento delle funzioni integrate, la riduzione dei loop aggiuntivi e la gestione efficace della memoria. Il benchmarking e la profilazione continui dovrebbero indirizzare gli sforzi di ottimizzazione, garantendo che i progressi del codice corrispondano ai requisiti prestazionali del mondo reale. Per garantire il successo del progetto a lungo termine e ridurre la possibilità di introdurre nuovi problemi, l’ottimizzazione del codice dovrebbe essere costantemente bilanciata con gli obiettivi di leggibilità e manutenibilità del codice.