8. Erori și excepții¶
Până la momentul de față, mesajele de eroare au cam fost trecute cu vederea, însă, dacă ați încercat toate exemplele, probabil că v-ați întâlnit cu ele. Există (cel puțin) două feluri distincte de erori: erorile de sintaxă și excepțiile.
8.1. Erori de sintaxă¶
Erorile de sintaxă, cunoscute și ca erori de parsare, sunt, de obicei, cele mai obișnuite tipuri de plângeri pe care vi le va adresa interpretorul cât timp n-ați terminat de învățat Python-ul:
>>> while True print('Salutare, lume')
File "<stdin>", line 1
while True print('Salutare, lume')
^^^^^
SyntaxError: invalid syntax
Parserul (parte a interpretorului) afișează linia care l-a supărat și îi adaugă vârfuri de săgeată îndreptate către porțiunea din linie unde a găsit o eroare. Remarcați că porțiunea indicată nu este, neapărat, cea în care trebuie făcute corecturi. În exemplu, greșeala este detectată în dreptul funcției print()
, iar aceasta deoarece semnul de punctuație două puncte (':'
) lipsește chiar din fața numelui funcției.
Numele fișierului (în cazul nostru, <stdin>
) și numărul liniei de cod sunt și ele afișate pentru ca dumneavoastră să știți de unde să începeți căutarea erorii atunci când codul eronat provine dintr-un fișier.
8.2. Excepții¶
Chiar și atunci când o instrucțiune ori o expresie sunt corecte sintactic, ele pot cauza erori dacă încercăm să le executăm. Erorile detectate în timpul execuției codului se numesc excepții și nu sunt, obligatoriu, fatale: vom învăța în curând cum să le manevrăm în programele scrise în Python. Totuși, majoritatea excepțiilor nu sunt tratate de codul programului, conducând la mesaje de eroare aidoma celui arătat aici:
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
10 * (1/0)
~^~
ZeroDivisionError: division by zero
>>> 4 + varză*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
4 + varză*3
^^^^^
NameError: name 'varză' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
'2' + 2
~~~~^~~
TypeError: can only concatenate str (not "int") to str
Ultimul rând al mesajului de eroare ne informează despre ce s-a întâmplat. Excepțiile sunt de tipuri diferite, iar tipul lor este afișat ca parte a mesajului: tipurile excepțiilor din exemplu sunt ZeroDivisionError
, NameError
și TypeError
. Șirul de caractere afișat pe post de tip al excepției este numele excepției predefinite care s-a produs. Acest comportament al interpretorului este regulă în ceea ce privește excepțiile predefinite, însă se poate să nu se adeverească și pentru indiferent care excepție definită de utilizator (chiar dacă reprezintă o convenție foarte utilă). Numele de excepții standard sunt identificatori predefiniți (dar nu cuvinte cheie rezervate).
Restul rândului de mesaj oferă detalii bazate pe tipul de excepție și pe cauza (producerii) ei.
Partea precedentă a mesajului de eroare ne arată contextul în care a apărut excepția, sub forma derulării (de la englezescul, ca jargon, traceback) unei stive. De obicei, această parte va conține derularea liniilor (rândurilor) de conținut ale stivei; cu toate acestea, nu vor fi afișate rândurile citite de la intrarea standard.
În Built-in Exceptions se găsesc lista excepțiilor predefinite împreună cu semnificațiile acestora.
8.3. Tratarea excepțiilor¶
Putem scrie programe care să trateze (doar) anumite excepții. Haideți să aruncăm o privire asupra exemplului de mai jos, în care utilizatorului i se cere să introducă numere până când un număr întreg convenabil va fi tastat, îngăduindu-i-se, în schimb, să oprească execuția programului oricând dorește (cu combinația de taste Control-C sau cu orice permite sistemul de operare folosit); trebuie remarcat că întreruperea (execuției) produsă de utilizator va fi semnalată prin ridicarea (de la englezescul raising; sau lansarea) excepției (de tipul) KeyboardInterrupt
.
>>> while True:
... try:
... x = int(input("Vă rog să introduceți un număr: "))
... break
... except ValueError:
... print("Vai! Acest număr nu e bun. Mai încercați...")
...
Instrucțiunea try
funcționează după cum urmează.
Prima se execută clauza try (adică, grupul de instrucțiuni situate între cuvintele-cheie
try
șiexcept
).Dacă nu sunt întâlnite excepții, atunci se va sări peste clauza except, execuția instrucțiunii
try
încheindu-se.Dacă survine vreo excepție în timpul execuției clauzei
try
, atunci se va sări peste restul clauzei. Presupunând că tipul excepției se potrivește (de la englezescul match) numelui de (tip de) excepție scris după cuvântul-cheieexcept
, se va executa clauza except, apoi execuția codului Python va trece la instrucțiunile situate după blocul try/except.În cazul când numele excepției survenite nu se potrivește cu cel precizat în clauza except, acest nume va fi transmis către instrucțiunile
try
exterioare; dacă nu va fi găsit niciun manipulator de excepție (de la englezescul handler; sau procedură de tratare a excepției) convenabil, atunci ne vom afla în fața unei excepții netratate (de către codul nostru Python; sau a unei excepții neinterceptate) iar execuția programului se va încheia brusc, furnizându-se un mesaj de eroare.
O instrucțiune try
poate avea mai mult de o singură clauză except, deținând, din acest motiv, manipulatori (de excepție) pentru felurite excepții. Cel mult unul dintre acești manipulatori va fi executat. Un manipulator tratează acea excepție care survine în timpul execuției clauzei try corespunzătoare, nu și acele (eventuale) excepții care intervin în ceilalți manipulatori din aceeași instrucțiune try
. O clauză except poate numi mai multe excepții, sub forma unui tuplu încadrat de paranteze rotunde, precum:
... except (RuntimeError, TypeError, NameError):
... pass
O clasă (numită) într-o clauză except
li se potrivește acelor excepții care sunt instanțe ale clasei însăși ori ale vreuneia din clasele derivate din clasa în cauză (dar nu și invers – o clauză except care conține numele unei clase derivate nu se va potrivi cu nicio instanță a vreunei clase de bază). De exemplu, codul următor va afișa B, C, D în exact această ordine:
class B(Exception):
pass
class C(B):
pass
class D(C):
pass
for clase in [B, C, D]:
try:
raise clase()
except D:
print("D")
except C:
print("C")
except B:
print("B")
Observați că, dacă am fi inversat clauzele except (așezând-o pe except B
prima), atunci s-ar fi afișat B, B, B — deoarece prima clauză except potrivită (excepțiilor) s-ar fi declanșat.
Atunci când intervine o excepție, ei îi pot fi asociate diverse valori, cunoscute și sub numele de argumente ale excepției în cauză. Atât existența unor asemenea argumente cât și tipurile lor depind de tipul de excepție.
Clauza except poate specifica o variabilă la dreapta numelui excepției. Această variabilă este legată de instanța excepției, excepția având, de obicei, un atribut args
în care sunt stocate asemenea valori (argumente ale excepției). Din rațiuni de utilitate, tipurile builtin (predefinite) de excepții definesc metoda __str__()
ca să putem afișa argumentele unor asemenea excepții fără să mai fie nevoie de accesarea în mod explicit a lui .args
.
>>> try:
... raise Exception('șuncă presată', 'ouă')
... except Exception as hrană:
... print(type(hrană)) # tipul de excepție
... print(hrană.args) # valorile stocate în .args
... print(hrană) # __str__ ne permite să afișăm direct argumentele,
... # însă poate fi suprascrisă în moștenitorii clasei
... x, y = hrană.args # despachetarea lui args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('șuncă presată', 'ouă')
('șuncă presată', 'ouă')
x = șuncă presată
y = ouă
Șirul returnat de metoda __str__()
a unei excepții va fi afișat ca ultimă parte (cea de «detalii») a mesajului de eroare produs de intervenția unei excepții netratate.
BaseException
este clasa de bază (superclasa) a tuturor excepțiilor. Una din moștenitoarele sale, Exception
, este clasa de bază a tuturor excepțiilor non-fatale. Excepțiile care nu-i sunt subclase (urmași) lui Exception
nu sunt, de obicei, tratate (cu manipulatori de excepție), deoarece ele sunt întrebuințate la a ne arăta că execuția programului trebuie oprită. Asemenea excepții includ SystemExit
, ridicată de sys.exit()
, respectiv KeyboardInterrupt
, ridicată atunci când utilizatorul dorește să întrerupă programul.
Exception
poate fi utilizată pe post de înlocuitor (de excepții specifice) capabil să intercepteze (de la englezescul catch) aproape orice fel de excepție. Cu toate acestea, bunele practici recomandă să fim cât mai la obiect cu putință vizavi de tipurile de excepții pe care suntem preocupați să le tratăm, permițându-le, în schimb, celorlalte excepții să se propage.
Șablonul (cel mai) obișnuit de tratare a lui Exception
este fie prin afișarea excepției fie prin jurnalizarea ei, urmată de o nouă ridicare a excepției (ceea ce îi va permite unui apelant — al codului — să trateze, la rândul său, excepția):
import sys
try:
f = open('fișierul_meu.txt')
s = f.readline()
i = int(s.strip())
except OSError as eroare:
print("Eroare SO:", eroare)
except ValueError:
print("Data nu poate fi convertită într-un număr întreg.")
except Exception as eroare:
print(f"O (neașteptată) {eroare=}, {type(eroare)=}")
raise
Instrucțiunea try
… except
dispune și de o clauză else opțională, care, dacă o introducem în programul nostru, trebuie să fie poziționată după toate clauzele except. Ea este utilă atunci când execuția clauzei try nu va ridica nicio excepție. Ca exemplu:
for argumentul in sys.argv[1:]:
try:
f = open(argumentul, 'r')
except OSError:
print('nu poate fi deschis', argumentul)
else:
print(argumentul, 'are', len(f.readlines()), 'linii')
f.close()
Întrebuințarea clauzei else
este mai nimerită, în practică, decât adăugirile de cod în corpul clauzei try
și aceasta în special pentru că se evită interceptarea unor excepții care să nu fi fost ridicate chiar de către codul pe care l-am protejat cu instrucțiunea try
… except
.
Manipulatorii de excepții nu tratează doar acele excepții care intervin direct în codul din clauza try, ci și pe acelea care survin în codul funcțiilor apelate (fie și indirect) în cadrul clauzei try. Precum aici (expresia englezească run-time, de jargon informatic, se poate traduce prin în timpul execuției):
>>> def cod_care_nu_funcționează():
... x = 1/0
...
>>> try:
... cod_care_nu_funcționează()
... except ZeroDivisionError as eroarea:
... print('Tratând o eroare din timpul execuției:', eroarea)
...
Tratând o eroare din timpul execuției: division by zero
8.4. Ridicând excepții¶
Instrucțiunea raise
îi îngăduie programatorului să forțeze apariția unei anumite excepții. De exemplu:
>>> raise NameError('Salutare')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
raise NameError('Salutare')
NameError: Salutare
Argumentul (unic al) lui raise
indică excepția care urmează a fi ridicată. Aceasta trebuie să fie sau instanța unei (clase de) excepții sau chiar o clasă de excepții (adică, o clasă derivată din BaseException
, precum Exception
ori vreunul din urmașii ei). Atunci când se transmite (drept argument unic) o clasă de excepții, aceasta va fi instanțiată în mod implicit prin apelarea constructorului fără argumente de care dispune:
raise ValueError # o prescurtare a lui 'raise ValueError()'
Presupunând că doriți să știți dacă s-a ridicat vreo excepție însă nu vă interesează și să o tratați, puteți folosi forma simplificată a instrucțiunii raise
în cadrul căreia vi se permite relansarea excepției în cauză:
>>> try:
... raise NameError('Salutare')
... except NameError:
... print('Tocmai ne-a depășit o excepție!')
... raise
...
Tocmai ne-a depășit o excepție!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise NameError('Salutare')
NameError: Salutare
8.5. Înlănțuirea excepțiilor¶
Atunci când o excepție netratată survine în interiorul unei secțiuni (clauze) except
, excepția care chiar este tratată îi va fi atașată, respectiv va fi menționată în mesajul de eroare:
>>> try:
... open("baza_de_date.sqlite")
... except OSError:
... raise RuntimeError("nu putem trata eroarea")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
open("baza_de_date.sqlite")
~~~~^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'baza_de_date.sqlite'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError("nu putem trata eroarea")
RuntimeError: nu putem trata eroarea
Pentru a indica faptul că o excepție este consecința directă a altei excepții, instrucțiunea raise
dispune de clauza opțională from
:
# excepția este fie instanța unei excepții fie None.
raise RuntimeError from excepția
Această clauză ne poate fi de folos atunci când transformăm excepții. Ca aici:
>>> def funcția():
... raise ConnectionError
...
>>> try:
... funcția()
... except ConnectionError as excepția:
... raise RuntimeError('Nu putem deschide baza de date') from excepția
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
funcția()
~~~~~~~^^
File "<stdin>", line 2, in funcția
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError('Nu putem deschide baza de date') from excepția
RuntimeError: Nu putem deschide baza de date
Tot ea ne ajută să dezafectăm înlănțuirea automată de excepții, cu ajutorul idiomului from None
:
>>> try:
... open('baza_de_date.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError from None
RuntimeError
Pentru mai multe informații privitoare la mecanismul înlănțuirii, vezi Built-in Exceptions.
8.6. Excepții definite de utilizator¶
Programatorii își pot denumi propriile excepții prin crearea unei noi clase de excepții (a se vedea Clase pentru mai multe despre clase în Python). Excepțiile derivă, în mod obișnuit, din clasa Exception
, atât direct cât și indirect.
Clasele de excepții pot realiza tot ce poate realiza oricare altă clasă în Python, însă se recomandă să fie păstrate simple, dispunând doar de un număr limitat de atribute care să le permită manipulatorilor de excepții asociați lor să extragă informații despre excepția survenită.
Majoritatea excepțiilor se definesc cu nume terminate în „Error” (Eroare), aidoma denumirii excepțiilor standard.
Multe module standard își definesc propriile excepții pentru a raporta erorile ce pot apărea la execuția funcțiilor conținute în modulele respective.
8.7. Definirea unor acțiuni de curățare¶
Instrucțiunea try
posedă încă o clauză opțională, destinată (mai ales) acțiunilor de curățare care trebuie realizate în absolut toate împrejurările. Cum ar fi:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Adio, lume!')
...
Adio, lume!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
Atunci când clauza finally
este prezentă, această clauză finally
va fi executată ca ultima activitate din cadrul instrucțiunii try
. Codul din clauza finally
va fi rulat indiferent dacă instrucțiunea try
a condus sau nu la apariția de excepții. La punctele care urmează, discutăm despre situațiile mai complicate când poate apărea vreo excepție:
Dacă survine o excepție în timpul executării clauzei
try
, excepția în cauză poate fi tratată de una din clauzeleexcept
. Presupunând că excepția nu va fi interceptată de niciuna din clauzeleexcept
, ea va fi relansată după încheierea execuției clauzeifinally
.Se poate ca vreo excepție să survină când este executată fie una din clauzele
except
fie clauzaelse
. Și aici, excepția va fi relansată după ce se va încheia executarea codului din clauzafinally
.Dacă în clauza
finally
se execută vreuna din instrucțiunilebreak
,continue
orireturn
, eventualele excepții nu vor mai fi relansate.Atunci când, la execuția codului din instrucțiunea
try
întâlnim vreuna din instrucțiunilebreak
,continue
saureturn
, clauzafinally
va fi executată chiar înainte de execuția oricăreia din instrucțiunilebreak
,continue
orireturn
.Dacă clauza
finally
include vreo instrucțiunereturn
, atunci valoarea returnată va fi cea dată de instrucțiuneareturn
a respectivei clauzefinally
și nu valoarea dată de eventuala instrucțiunereturn
a clauzeitry
.
Un exemplu:
>>> def întoarce_valoare_de_adevăr():
... try:
... return True
... finally:
... return False
...
>>> întoarce_valoare_de_adevăr()
False
Alt exemplu, mai sofisticat:
>>> def împărțire(x, y):
... try:
... rezultat = x / y
... except ZeroDivisionError:
... print("împărțire la zero!")
... else:
... print("rezultatul este", rezultat)
... finally:
... print("executăm clauza finally")
...
>>> împărțire(2, 1)
rezultatul este 2.0
executăm clauza finally
>>> împărțire(2, 0)
împărțire la zero!
executăm clauza finally
>>> împărțire("2", "1")
executăm clauza finally
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
împărțire("2", "1")
~~~~~~~~~^^^^^^^^^^
File "<stdin>", line 3, in împărțire
rezultat = x / y
~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
După cum puteți vedea, clauza finally
a fost executată în toate situațiile. Excepția TypeError
, ridicată de încercarea de a realiza împărțirea a două șiruri de caractere, nu este tratată de clauza except
și, prin urmare, a fost relansată după ce s-a încheiat execuția clauzei finally
.
În aplicațiile din lumea reală (sau de producție), clauza finally
se întrebuințează (mai ales) la eliberarea resurselor externe (precum fișierele sau conexiunile la rețeaua Internet), indiferent dacă accesul la acestea a putut fi realizat.
8.8. Acțiuni de curățare predefinite¶
Anumite obiecte definesc acțiuni de curățare standard, acestea urmând să fie realizate atunci când nu mai avem nevoie de obiectul în cauză, indiferent dacă operația care a folosit obiectul respectiv s-a putut sau nu realiza. Să aruncăm o privire asupra exemplului de mai jos, în care se încearcă deschiderea unui fișier, urmată de afișarea conținutului acestuia.
for rândul in open("fișierul_meu.txt"):
print(rândul, end="")
Scăparea (programatică) din acest cod este aceea că fișierul va fi lăsat deschis pe o durată nedeterminată după ce interpretorul a terminat de executat codul. O asemenea situație nu le impietează prea mult unor scripturi simple, însă poate deveni problematică pentru aplicații complexe. Instrucțiunea with
le facilitează obiectelor precum fișierele o utilizare corectă, astfel încât resursele blocate de ele să fie eliberate prompt și corect, întotdeauna.
with open("fișierul_meu.txt") as fișier:
for rândul in fișier:
print(rândul, end="")
Odată ce instrucțiunea a fost executată, fișierul fișier va fi închis, în mod garantat, inclusiv în situația în care ar fi existat dificultăți la procesarea rândurilor sale. Acele obiecte care, aidoma obiectelor fișier, oferă acțiuni de curățare (eliberare de resurse) predefinite, trebuie să indice o atare capacitate în documentația aferentă.
8.10. Îmbogățind excepțiile cu notițe¶
Atunci când o excepție este creată, urmând a fi lansată, ea se inițializată, de obicei, cu informații referitoare la eroarea tocmai survenită. În anumite situații, ne interesează să le adăugăm informațiilor în cauză (ale instanței excepției) detalii suplimentare, însă doar după ce am interceptat excepția. Pentru un atare scop, excepțiile dispun de metoda add_note(notițe)
, care acceptă un șir de caractere drept argument și îl adaugă listei de notițe ale excepției. Derularea tipică (a stivei) afișează toate notițele, în ordinea introducerii lor, în continuarea excepției.
>>> try:
... raise TypeError('tip greșit')
... except Exception as excepția:
... excepția.add_note('Mai adaug informații')
... excepția.add_note('Mai adaug și alte informații')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('tip greșit')
TypeError: tip greșit
Mai adaug informații
Mai adaug și alte informații
>>>
Cu titlu de exemplu, atunci când așezăm împreună variate excepții într-un grup de excepții, este de așteptat să ne dorim să adăugăm informații contextuale pentru erorile individuale (luate în parte). În codul de mai jos, fiecare excepție din grup va fi urmată de o notiță în care se precizează când a avut loc eroarea respectivă.
>>> def funcția():
... raise OSError('operația a dat greș')
...
>>> excepțiile = []
>>> for i in range(3):
... try:
... funcția()
... except Exception as excepția:
... excepția.add_note(f'A survenit la iterația {i+1}')
... excepțiile.append(excepția)
...
>>> raise ExceptionGroup('Au apărut probleme', excepțiile)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('Au apărut probleme', excepțiile)
| ExceptionGroup: Au apărut probleme (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| funcția()
| ~~~~~~~^^
| File "<stdin>", line 2, in funcția
| raise OSError('operația a dat greș')
| OSError: operația a dat greș
| A survenit la iterația 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| funcția()
| ~~~~~~~^^
| File "<stdin>", line 2, in funcția
| raise OSError('operația a dat greș')
| OSError: operația a dat greș
| A survenit la iterația 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| funcția()
| ~~~~~~~^^
| File "<stdin>", line 2, in funcția
| raise OSError('operația a dat greș')
| OSError: operația a dat greș
| A survenit la iterația 3
+------------------------------------
>>>