timeit - Cronometra l'Esecuzione di Piccoli Frammenti di Codice Python.

Scopo: Cronometra l'esecuzione di piccoli frammenti di codice Python

Il modulo timeit fornisce una semplice interfaccia per determinare il tempo di esecuzione di piccoli frammenti di codice Python. Usa una funzione di cronometraggio specifica alla piattaforma per fornire un calcolo del tempo più accurato possibile e ridurre l'impatto dei costi di partenza e chiusura dello script rispetto al calcolo del tempo eseguendo il codice ripetutamente.

Contenuti del Modulo

timeit definisce una singola classe pubblica, Timer. Il costruttore per Timer riceve una istruzione da cronometrare e una istruzione di "impostazione" (usata ad esempio per inizializzare variabili). Le istruzioni Python dovrebbero essere stringhe e possono includere ritorni a capo.

Il metodo timeit() esegue una volta l'istruzione di impostazione, poi esegue l'istruzione primaria ripetutamente e ritorna il valore del tempo trascorso. Un argomento di timeit() regola il numero di ripetizioni per l'istruzione; nella modalità predefinita è 1.000.000.

Esempio Base

Per illustrare come vengono usati i vari argomenti per Timer, ecco un semplice esempio che stampa un valore specificato quando viene eseguita ciascuna istruzione.

# timeit_example.py

import timeit

# using setitem
t = timeit.Timer("print('istruzione principale')", "print('impostazione')")

print('TIMEIT:')
print(t.timeit(2))

print('RIPETE:')
print(t.repeat(3, 2))

Quando eseguito, il risultato mostra l'esito delle chiamate ripetute a print().

$ python3 timeit_example.py

TIMEIT:
impostazione
istruzione principale
istruzione principale
6.4179998844338115e-06
RIPETE:
impostazione
istruzione principale
istruzione principale
impostazione
istruzione principale
istruzione principale
impostazione
istruzione principale
istruzione principale
[6.894999842188554e-06, 6.880000000819564e-06, 6.137000127637293e-06]

timeit() esegue l'istruzione di impostazione una volta, poi chiama l'istruzione principale un numero di volte pari a count. Ritorna un singolo valore a virgola mobile che rappresenta il valore cumulativo del tempo trascorso nell'esecuzione dell'istruzione principale.

Quando viene usato repeat(), esso chiama timeit() diverse volte (3 in questo caso) e tutte le risposte sono ritornate in una lista.

Conservare i Valori in un Dizionario

Questo esempio più complesso confronta il tempo trascorso per popolare un dizionario con un gran numero di valori utilizzando vari metodi. Per prima cosa sono necessarie alcune costanti per configurare Timer. La variabile setup_statement inizializza una lista di tuple che contengono stringhe e interi che saranno usati dalle istruzioni principali per costruire dizionari usando le stringhe come chiave e conservando gli interi come valori associati.

Il programma di esempio è timeit_dictionary.py e viene di seguito illustrato nelle sue componenti principali:

# Alcune costanti
range_size = 1000
count = 1000
setup_statement = ';'.join([
    "l = [(str(x), x) for x in range(1000)]",
    "d = {}",
])

Una funzione di utilità, show_results(), viene definita per stampare i risultati in un formato consono. Il metodo timeit() ritorna il tempo trascorso per eseguire l'istruzione ripetutamente. Il risultato di show_results() lo converte nel tempo trascorso per iterazione, successivamente riduce il valore alla media di tempo trascorso per inserire un valore nel dizionario.

def show_results(result):
    "Stampa i microsecondi per passaggio e per elemento"
    global count, range_size
    per_pass = 1000000 * (result / count)
    print('{:6.2f} usec/pass'.format(per_pass), end=' ')
    per_item = per_pass / range_size
    print('{:6.2f} usec/item'.format(per_item))


print("{} elementi".format(range_size))
print("{} iterazioni".format(count))
print()

Per stabilire una linea base, la prima configurazione provata usa __setitem__(). Tutte le altre varianti evitano di sovrascrivere valori se sono già nel dizionario, quindi questa versione dell'esempio dovrebbe essere la più veloce.

Il primo argomento di Timer è una stringa multi riga, con spazi conservati in modo che possa essere interpretata correttamente quando eseguita. Il secondo argomento è una costante impostata per inizializzare la lista di valori e il dizionario.

# Usa __setitem__ senza prima verificare se i valori esistono
print('__setitem__:', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            d[s] = i
        """),
    setup_statement,
)
show_results(t.timeit(number=count))

La variante successiva usa setdefault() per assicurarsi che i valori già nel dizionario non vengano sovrascritti.

# Usa setdefault
print('setdefault :', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            d.setdefault(s, i)
        """),
    setup_statement,
)
show_results(t.timeit(number=count))

Questo metodo aggiunge il valore solo se viene sollevata una eccezione KeyError durante la ricerca per un valore esistente.

# Usa eccezioni
print('KeyError   :', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            try:
                existing = d[s]
            except KeyError:
                d[s] = i
        """),
    setup_statement,
)
show_results(t.timeit(number=count))

L'ultimo metodo usa in per determinare se un dizionario ha una particolare chiave.

# Usa "in"
print('"not in"   :', end=' ')
t = timeit.Timer(
    textwrap.dedent(
        """
        for s, i in l:
            if s not in d:
                d[s] = i
        """),
    setup_statement,
)

Quando lo script viene eseguito produce il seguente risultato:

$ python3 timeit_dictionary.py

1000 elementi
1000 iterazioni

__setitem__:  64.32 usec/pass   0.06 usec/item
setdefault : 198.63 usec/pass   0.20 usec/item
KeyError   :  53.17 usec/pass   0.05 usec/item
"not in"   :  43.53 usec/pass   0.04 usec/item

Il risultato potrebbe variare in base alla macchina nella quale viene eseguito lo script e in base a quali altri programmi sono in esecuzione nel sistema. Si esperimenti con le variabili range_size e count, visto che combinazioni diverse produrranno diversi risultati.

Da Riga di Comando

Oltre all'interfaccia da programma, timeit fornisce una interfaccia da riga di comando per provare i moduli senza orchestrazione.

Per eseguire il modulo si utilizzi l'opzione -m dell'interprete Pyhton per trovare il modulo e considerarlo come il programma principale. Per ottenere aiuto per il modulo ad esempio:

$ python3 -m timeit --help

Tool for measuring execution time of small code snippets.

This module avoids a number of common traps for measuring execution
times.  See also Tim Peters' introduction to the Algorithms chapter in
the Python Cookbook, published by O'Reilly.

...

L'argomento statement funziona in modo leggermente diverso da riga di comando rispetto all'argomento per Timer. Invece di usare una lunga stringa, si passino le singole righe di istruzione come argomenti separati da riga di comando. Per indentare le righe (come per l'interno di un ciclo), si inseriscano spazi nella stringa racchiudendo la stessa tra apici.

$ python3 -m timeit -s "d={}" "for i in range(1000):" "  d[str(i)] = i"

1000 loops, best of 3: 258 usec per loop

E' anche possibile definire una funzione con codice più complesso, poi chiamare la funzione da riga di comando.

# timeit_setitem.py

def test_setitem(range_size=1000):
    l = [(str(x), x) for x in range(range_size)]
    d = {}
    for s, i in l:
        d[s] = i

Per eseguire la prova, passare il codice che importa i moduli ed esegue la funzione di prova.

$ python3 -m timeit -s "import timeit_setitem; timeit_setitem.test_setitem()"

100000000 loops, best of 3: 0.00778 usec per loop

Vedere anche:

timeit
La documentazione della libreria standard per questo modulo.
profile
Il modulo profile è anche utile per analisi di prestazioni
Monotonic Clocks
Discussione sul clock monotonico dal modulo time