Chi programma di solito cerca di scrivere software perfetto, e di solito fallisce.
Le cose vanno storte, inaspettatamente, e dobbiamo essere in grado di affrontarlo.
Alcuni progettisti di linguaggi ritengono che la priorità sia individuare un errore il più rapidamente possibile, poi terminare l'esecuzione con un messaggio informativo che aiuti il debug.
I linguaggi per la data science tendono ad adottare un approccio più articolato. Alcuni errori sono così gravi che la terminazione immediata è necessaria, ma spesso è meglio segnalare un problema come qualcosa da affrontare più tardi, e poi continuare l'esecuzione.
Nel concetto Nullità abbiamo visto che Julia fornisce vari segnaposto per valori problematici: missing, NaN e Inf.
Se questi siano un approccio migliore rispetto alla terminazione del programma in una situazione particolare è una questione di giudizio del programmatore.
Una precisazione sulla nomenclatura prima di entrare nei dettagli: la documentazione di Julia considera le parole «error» e «exception» in gran parte intercambiabili. Anche il contenuto che segue potrebbe essere altrettanto incoerente.
A questo punto del percorso, avrai sicuramente visto molti messaggi di errore da Julia. Per esempio:
julia> Int(3.14)
ERROR: InexactError: Int64(3.14)
Provare a convertire un numero in virgola mobile in un intero comporta una perdita di precisione, quindi otteniamo un InexactError.
InexactError è un tipo, uno dei diversi (attualmente 25) integrati in Julia come standard.
Sono tutti sottotipi di Exception:
julia> supertype(InexactError)
Exception
throw()Alcuni dei tipi di errore standard potrebbero essere utili da generare nel codice.
Come tutti i tipi concreti, gli errori hanno costruttori. Accettano una varietà di argomenti, quindi controlla la documentazione per quello che vuoi usare.
julia> DomainError(42, "out of range")
DomainError(42, "out of range")
Per usare l'errore, avvolgi il costruttore in una funzione throw():
julia> throw(DomainError(42, "out of range"))
ERROR: DomainError with 42:
out of range
error()Per un approccio rapido e senza troppi fronzoli, la funzione error() può essere comoda.
Accetta una stringa (o i componenti di una stringa) come argomento:
julia> happy = false;
julia> happy || error("😞 something went wrong")
ERROR: 😞 something went wrong
Creare nuovi tipi di errore è in linea di principio molto facile.
Basta aggiungere un altro sottotipo di Exception:
julia> struct MyError <: Exception end
julia> throw(MyError)
ERROR: MyError
L'idea di base di un'asserzione è «questa affermazione dovrebbe essere vera, quindi protesta a gran voce se è falsa». Il valore di questo sta principalmente durante il debug, dato che il codice di produzione non dovrebbe mai fallire un'asserzione.
Nel concetto Tipi abbiamo visto che possiamo aggiungere asserzioni di tipo, per esempio per controllare il tipo restituito da una funzione.
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
Più in generale, la macro @assert ci permette di verificare qualsiasi espressione che produca un valore booleano:
julia> n = 22;
julia> @assert isodd(n) "n must be odd"
ERROR: AssertionError: n must be odd
try...catch
Alcuni errori sono necessariamente fatali, ma spesso ci aspettiamo che il programma si riprenda con grazia.
Per impostazione predefinita, un errore termina immediatamente la funzione corrente, e l'errore (con qualsiasi messaggio informativo) viene passato alla funzione chiamante.
Questo continua su per lo stack di chiamate, finché il codice di livello superiore termina con un messaggio di errore.
In qualsiasi momento, l'errore può essere intercettato con un blocco try...catch che prova a gestirlo.
julia> n = -1;
julia> try
log_n = log(n)
catch problem
if problem isa DomainError # number out of range
# See next section for more on @warn and @info
@warn "you may have supplied a negative real number: $n"
@info "trying with complex argument"
log_n = log(Complex(n)) # fallback calculation
elseif problem isa MethodError # no idea what n is
@error "please supply a valid argument"
else
rethrow() # the error could be anything else
end
end
┌ Warning: you may have supplied a negative real number: -1
└ @ Main REPL[3]:5
[ Info: trying with complex argument
0.0 + 3.141592653589793im # success
Nell'esempio sopra, log(n) richiede che n sia un valore reale positivo oppure un valore complesso qualsiasi.
Il try ... catch intercetta i problemi con valori reali negativi, restituendo la risposta complessa corretta iπ in notazione matematica.
Se fornisci, per esempio, un argomento di tipo stringa, non c'è modo di recuperare se non chiedere all'utente di correggerlo.
Come ultima rete di sicurezza, abbiamo aggiunto rethrow() per tutto ciò che non è né DomainError né MethodError.
Nota: A volte un try...catch è quello che serve, ma evita di abusarne.
Se al suo posto si può usare un blocco if...else, sarà molto più performante che catturare eccezioni.
Nota che la funzione error(), discussa sopra, non va confusa con la macro @error.
La funzione genera un'eccezione, che verrà propagata su per lo stack di chiamate a meno che non venga intercettata.
La macro @error, insieme alle sue controparti @debug, @info e @warn, fa parte del modulo Logging ed è pensata per generare messaggi informativi senza alterare il flusso del programma.
L'output va al terminale per impostazione predefinita (con colori che ne indicano la gravità), anche se in un'applicazione reale ci sono molte altre possibilità.
julia> @warn "Something looks not quite right"
┌ Warning: Something looks not quite right
└ @ Main REPL[55]:1
julia> @error "Panic!"
┌ Error: Panic!
└ @ Main REPL[56]:1
Vedi anche l'esempio precedente, sotto try...catch.
Elena è la nuova responsabile qualità di una fabbrica di giornali. Dato che è appena arrivata in azienda, ha deciso di rivedere alcuni processi della fabbrica per capire cosa si potrebbe migliorare. Ha scoperto che i tecnici fanno molti controlli di qualità a mano. Vede una buona opportunità di automazione e chiede a te, sviluppatore freelance, di realizzare un software per monitorare alcune delle macchine.
La tua prima missione è scrivere un software per monitorare il livello di umidità della sala di produzione. C'è già un sensore collegato al software dell'azienda che restituisce periodicamente la percentuale di umidità della sala.
Devi implementare una funzione nel software che lanci un errore se la percentuale di umidità è troppo alta.
Se l'umidità è a un livello accettabile, verrà aggiunto un log informativo.
La funzione deve chiamarsi humiditycheck e prendere la percentuale di umidità come argomento.
Dovresti interrompere l'esecuzione con un ErrorException (il messaggio esatto non è importante, ma deve contenere il livello di umidità misurato) se la percentuale supera il 70%.
Altrimenti, aggiungi un log informativo con il messaggio "humidity level check passed: h%", dove h è la percentuale di umidità.
julia> humiditycheck(60)
[ Info: humidity level check passed: 60%
julia> humiditycheck(100)
ERROR: humidity check failed: 100%
Elena è molto soddisfatta del tuo primo incarico e ti chiede di occuparti del monitoraggio della temperatura delle macchine. Mentre chiacchieri con un tecnico, Greg, vieni a sapere che se la temperatura di una macchina supera i 500°C, i tecnici iniziano a preoccuparsi del surriscaldamento.
La macchina è dotata di un sensore che ne misura la temperatura interna. Devi sapere che il sensore è molto sensibile e spesso si rompe. In questo caso, i tecnici dovranno sostituirlo.
Il tuo compito è implementare una funzione temperaturecheck che prende la temperatura come argomento e che aggiunge un log se va tutto bene, oppure lancia un errore se il sensore è rotto o se la macchina inizia a surriscaldarsi. Sapendo che in seguito dovrai reagire in modo diverso a seconda dell'errore, ti serve un meccanismo per distinguere i due tipi di errore.
nothing.
In questo caso, dovresti interrompere l'esecuzione con un ArgumentError (il messaggio non è importante).DomainError che include la temperatura misurata."temperature check passed: t °C", dove t è la temperatura.julia> temperaturecheck(nothing)
ERROR: ArgumentError: sensor is broken
julia> temperaturecheck(800)
ERROR: DomainError with 800:
"overheating detected"
julia> temperaturecheck(500)
[ Info: temperature check passed: 500 °C
Per il prossimo compito, dovrai definire un errore più generale, in grado di catturare ogni caso. I dettagli dell'implementazione non sono importanti, basta che sia un errore e che si chiami MachineError. Sei libero di includere campi e messaggi come ritieni utile.
Ora che la tua macchina è in grado di rilevare gli errori e hai un errore personalizzato, aggiungi una funzione wrapper che possa riportare come funziona tutto. Oltre a restituire i log delle funzioni precedenti, questo wrapper dovrà anche aggiungere log a seconda del tipo o dei tipi di errore che si verificano.
ErrorException, va aggiunto un log di errore con il messaggio "humidity level check failed: h%", dove h è la percentuale di umidità.ArgumentError, va aggiunto un log di avviso con il messaggio "sensor is broken".DomainError, va aggiunto un log di errore con il messaggio "overheating detected: t °C", dove t è la temperatura.MachineError dopo che i log sono stati aggiunti.humiditycheck e temperaturecheck.Implementa una funzione machinemonitor() che prende l'umidità e la temperatura come argomenti.
julia> machinemonitor(42, 450)
[ Info: humidity level check passed: 42%
[ Info: temperature check passed: 450 °C
julia> machinemonitor(42, 550)
[ Info: humidity level check passed: 42%
┌ Error: overheating detected: 550 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(82, 521)
┌ Error: humidity level check failed: 82%
└ @ Main # output truncated
┌ Error: overheating detected: 521 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(42, nothing)
[ Info: humidity level check passed: 42%
┌ Warning: sensor is broken
└ @ Main # output truncated
Error: MachineError
Iscriviti a Exercism per imparare e padroneggiare Julia con 35 concetti128 esercizi e il mentoring di persone reali, tutto gratis.