trace - Seguire il Flusso del Programma

Scopo: Controlla quali istruzioni e funzioni sono eseguite quando il programma è in esecuzione per produrre informazioni di copertura e grafi di chiamata.

Il modulo trace è utile per comprendere il modo nel quale un programma viene eseguito. Osserva le istruzioni eseguite, produce report di copertura e aiuta a investigare le relazioni tra funzioni che si chiamano le une con le altre.

Programma di esempio

Questo programma verrà usato negli esempi che seguono. Importa un altro modulo chiamato recurse e da esso esegue una funzione.

# esempio_trace/main.py

from recurse import recurse


def main():
    print('Questo è il programma principale.')
    recurse(2)


if __name__ == '__main__':
    main()

La funzione recurse() chiama se stessa fino a che l'argomento level raggiunge 0.

# esempio_trace/recurse.py

def recurse(level):
    print('recurse({})'.format(level))
    if level:
        recurse(level - 1)


def not_called():
    print('Questa funzione non viene mai chiamata.')

Tracciare l'Esecuzione

E' facile usare trace direttamente dalla riga di comando. Le istruzioni che vengono eseguite mentre il programma gira vengono stampate se viene fornita l'opzione --trace. Questo esempio ignora anche la locazione della libreria standard di Python (riferita all'interprete Python utilizzato - in questo caso /usr/lib/python3.8 - n.d.t.) per evitare la tracciatura di importlib e altri moduli che potrebbero essere più interessanti da includere in un altro esempio, ma che in questo caso farebbero solo confusione nella stampa dei risultati.

$ python3 -m trace --ignore-dir=/usr/lib/python3.8 --trace esempio_trace/main.py

 --- modulename: main, funcname: <module>
main.py(3): from recurse import recurse
 --- modulename: recurse, funcname: <module>
recurse.py(3): def recurse(level):
recurse.py(9): def not_called():
main.py(6): def main():
main.py(11): if __name__ == '__main__':
main.py(12):     main()
 --- modulename: main, funcname: main
main.py(7):     print('Questo è il programma principale.')
Questo è il programma principale.
main.py(8):     recurse(2)
 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(2)
recurse.py(5):     if level:
recurse.py(6):         recurse(level - 1)
 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(1)
recurse.py(5):     if level:
recurse.py(6):         recurse(level - 1)
 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(0)
recurse.py(5):     if level:

La prima parte del risultato mostra le operazioni di impostazione effettuate da trace . Il resto mostra l'entrata in ciascuna funzione, compreso il modulo dove la funzione risiede, quindi le righe del file sorgente così come sono eseguite. recurse() viene eseguita tre volte, come ci si attende in base al modo di chiamata in main().

Copertura del Codice

Eseguendo trace da riga di comando con l'opzione --count verranno prodotte informazioni di copertura di codice, dettagliando quali righe sono state eseguite e quali sono state saltate. Visto che un programma complesso in genere è composto da diversi file, viene prodotto un report di copertura separato per ciascuno. Nella modalità predefinita i file di report sono scritti nella stessa directory del modulo, attribuendogli il nome del modulo con estensione .cover invece che .py.

$ python3 -m trace --count esempio_trace/main.py

Questo è il programma principale.
recurse(2)
recurse(1)
recurse(0)

Sono prodotti due file in output, esempio_trace/main.cover :

# esempio_trace/main.cover
    1: from recurse import recurse


    1: def main():
    1:     print('Questo è il programma principale.')
    1:     recurse(2)


    1: if __name__ == '__main__':
    1:     main()

ed esempio_trace/recurse.cover.

# esempio_trace/recurse.cover

    1: def recurse(level):
    3:     print('recurse({})'.format(level))
    3:     if level:
    2:         recurse(level - 1)


    1: def not_called():
           print('Questa funzione non viene mai chiamata.')
Sebbene la riga recurse(level): abbia un conteggio di 1, non significa che quella funzione è stata eseguita una sola volta. Significa che la definizione della funzione è stata eseguita una volta. Lo stesso vale per not_called(): visto che la definizione della funzione è comunque valutata anche se la stessa non viene mai chiamata.

E' anche possibile eseguire il programma diverse volte, forse con diverse opzioni, per salvare i dati di copertura e produrre un report combinato. La prima volta che trace viene eseguito con un file in uscita, riporta un errore quando tenta di caricare dati esistenti da combinare con i nuovi risultati prima di creare il file.

$ python3 -m trace --coverdir coverdir1 --count --file coverdir1/coverage_report.dat esempio_trace/main.py

Questo è il programma principale.
recurse(2)
recurse(1)
recurse(0)
Skipping counts file 'coverdir1/coverage_report.dat': [Errno 2] No such file or directory: 'coverdir1/coverage_report.dat'
$ python3 -m trace --coverdir coverdir1 --count --file coverdir1/coverage_report.dat esempio_trace/main.py

Questo è il programma principale.
recurse(2)
recurse(1)
recurse(0)
$ python3 -m trace --coverdir coverdir1 --count --file coverdir1/coverage_report.dat esempio_trace/main.py

Questo è il programma principale.
recurse(2)
recurse(1)
recurse(0)
$ ls coverdir1

coverage_report.dat
esempio_trace.main.cover
esempio_trace.recurse.cover
main.cover
recurse.cover

Per produrre report una volta che le informazioni di copertura sono registrate nei file .cover si usi l'opzione --report.

$ python3 -m trace --coverdir coverdir1 --report --summary --missing --file coverdir1/coverage_report.dat esempio_trace/main.py

lines   cov%   module   (path)
    6   100%   esempio_trace.main   (esempio_trace/main.py)
    6    83%   esempio_trace.recurse   (esempio_trace/recurse.py)

Visto che il programma viene eseguito tre volte, il report di copertura mostra valori tre volte più alti del primo report. L'opzione --summary aggiunge la percentuale di informazioni coperte al risultato. Il modulo recurse è coperto solo per l'83%. Guardando il file .cover per recurse si nota che il corpo della funzione not_called() in effetti non è mai eseguito, indicato dal prefisso >>>>>>.

# coverdir1/esempio_trace.recurse.cover

    3: def recurse(level):
    9:     print('recurse({})'.format(level))
    9:     if level:
    6:         recurse(level - 1)


    3: def not_called():
>>>>>>     print('Questa funzione non viene mai chiamata.')

Relazioni tra Chiamate

Oltre alle informazioni di copertura, trace raccoglie e riporta circa le relazioni tra funzioni che si chiamano le une con le altre.

Per una semplice lista delle funzioni chiamate si usi --listfuncs.

$ python3 -m trace --listfuncs esempio_trace/main.py | grep -v importlib

Questo è il programma principale.
recurse(2)
recurse(1)
recurse(0)

functions called:
filename: <frozen zipimport>, modulename: <frozen zipimport>, funcname: zipimporter.__init__
filename: esempio_trace/main.py, modulename: main, funcname: <module>
filename: esempio_trace/main.py, modulename: main, funcname: main
filename: esempio_trace/recurse.py, modulename: recurse, funcname: <module>
filename: esempio_trace/recurse.py, modulename: recurse, funcname: recurse

Per maggiori dettagli circa chi effettua la chiamata si usi -trackcalls.

$ python3 -m trace --listfuncs --trackcalls esempio_trace/main.py | grep -v importlib

Questo è il programma principale.
recurse(2)
recurse(1)
recurse(0)

calling relationships:

*** /usr/lib/python3.8/trace.py ***
  --> esempio_trace/main.py
    trace.Trace.runctx -> main.<module>

  --> esempio_trace/recurse.py

  --> <frozen zipimport>

*** <frozen zipimport> ***

*** esempio_trace/main.py ***
    main.<module> -> main.main
  --> esempio_trace/recurse.py
    main.main -> recurse.recurse

*** esempio_trace/recurse.py ***
    recurse.recurse -> recurse.recurse
--listfuncs--trackcalls onorano gli argomenti --ignore-dirs o --ignore-mods, quindi la parte di risultato da questo esempio è estratta usando grep.

Interfaccia di Programmazione

Per un maggior controllo sulla sua interfaccia, trace può essere chiamato da un altro programma tramite un oggetto Trace. Trace supporta l'impostazione di impianti e altre dipendenze prima di eseguire una singola funzione o un comando Python da tracciare.

# trace_run.py

import trace
from esempio_trace.recurse import recurse

tracer = trace.Trace(count=False, trace=True)
tracer.run('recurse(2)')

Visto che l'esempio traccia solo la funzione recurse() non sono incluse nel risultato le informazioni da main.py.

$ python3 trace_run.py

 --- modulename: trace_run, funcname: <module>
<string>(1):  --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(2)
recurse.py(5):     if level:
recurse.py(6):         recurse(level - 1)
 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(1)
recurse.py(5):     if level:
recurse.py(6):         recurse(level - 1)
 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(0)
recurse.py(5):     if level:

Lo stesso risultato può essere ottenuto anche con il metodo runfunc().

# trace_runfunc.py

import trace
from esempio_trace.recurse import recurse

tracer = trace.Trace(count=False, trace=True)
tracer.runfunc(recurse, 2)

runfunc() accetta argomenti arbitrari posizionali e nominativi, che sono passati alla funzione quando è chiamata dal tracciatore.

$ python3 trace_runfunc.py

 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(2)
recurse.py(5):     if level:
recurse.py(6):         recurse(level - 1)
 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(1)
recurse.py(5):     if level:
recurse.py(6):         recurse(level - 1)
 --- modulename: recurse, funcname: recurse
recurse.py(4):     print('recurse({})'.format(level))
recurse(0)
recurse.py(5):     if level:

Salvare i Dati del Risultato

Anche le informazioni di conteggio e copertura possono essere registrate, proprio come nella interfaccia da riga di comando. I dati devono essere salvati esplicitamente, usando l'istanza CoverageResults dall'oggetto Trace.

# trace_CoverageResults.py

import trace
from esempio_trace.recurse import recurse

tracer = trace.Trace(count=True, trace=False)
tracer.runfunc(recurse, 2)

results = tracer.results()
results.write_results(coverdir='coverdir2')

Questo esempio salva i risultati di copertura nella directory coverdir2.

$ python3 trace_CoverageResults.py

recurse(2)
recurse(1)
recurse(0)
$ find coverdir2

coverdir2
coverdir2/esempio_trace.recurse.cover

Il file in uscita contiene.

# coverdir2/esempio_trace.recurse.cover

>>>>>> def recurse(level):
    3:     print('recurse({})'.format(level))
    3:     if level:
    2:         recurse(level - 1)


>>>>>> def not_called():
>>>>>>     print('Questa funzione non viene mai chiamata.')

Per salvare i dati di conteggio per generare report, si usino gli argomenti infile e outfile di Trace.

# trace_report.py

import trace
from esempio_trace.recurse import recurse

tracer = trace.Trace(count=True,
                     trace=False,
                     outfile='trace_report.dat')
tracer.runfunc(recurse, 2)

report_tracer = trace.Trace(count=False,
                            trace=False,
                            infile='trace_report.dat')
results = tracer.results()
results.write_results(summary=True, coverdir='/tmp')

Si passi un nome di file ad infile per leggere dati salvati in precedenza, e un nome di file ad outfile per scrivere i nuovi risultati dopo la tracciatura. Se infile ed outfile sono uguali, si crea l'effetto di aggiornare il file con dati cumulativi.

$ python3 trace_report.py

recurse(2)
recurse(1)
recurse(0)
lines   cov%   module   (path)
    6    50%   esempio_trace.recurse   (.../esempio_trace/recurse.py)

Opzioni

Il costruttore di Trace riceve parecchi parametri opzionali per controllare il comportamento in fase di esecuzione.

PARAMETRO TIPO DESCRIZIONE
count Booleano Attiva il conteggio del numero di riga. Predefinito True
countfuncs Booleano Attiva l'elenco delle funzioni chiamate in fase di esecuzione. Predefinito False
countcallers Booleano Attiva la tracciatura di chiamanti e chiamati. Predefinito False
ignoremods Sequenza Lista di moduli e pacchetti da ignorare nella tracciatura della copertura. Predefinito una tupla vuota
ignoredirs Sequenza Lista di directory contenenti moduli e pacchetti da ignorare. Predefinito una tupla vuota
infile Stringa Nome del file contenente i valori di conteggio in cache. Predefinito None
outfile Stringa Nome del file da usare per conservare i file di conteggio. Predefinito None e i dati non sono conservati.

Vedere anche:

trace
La documentazione della libreria standard per questo modulo.
Tracciare un programma in fase di esecuzione
Il modulo sys include servizi per aggiungere una funzione di tracciatura personalizzata all'interprete in fase di esecuzione (in corso di traduzione)
coverage.py
Il modulo coverage di Neil Batchelder.
figleaf
L'applicazione di copertura di Titus Brown