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('Hello world')
     File "<stdin>", line 1
       while True print('Hello world')
                      ^
   SyntaxError: invalid syntax

The parser repeats the offending line and displays a little 'arrow'
pointing at the earliest point in the line where the error was
detected.  The error is caused by (or at least detected at) the token
*preceding* the arrow: in the example, the error is detected at the
function "print()", since a colon ("':'") is missing before it.  File
name and line number are printed so you know where to look in case the
input came from a script.


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>
   ZeroDivisionError: division by zero
   >>> 4 + spam*3
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   NameError: name 'spam' is not defined
   >>> '2' + 2
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   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 Excepții predefinite 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("Please enter a number: "))
   ...         break
   ...     except ValueError:
   ...         print("Oops!  That was no valid number.  Try again...")
   ...

Instrucțiunea "try" funcționează după cum urmează.

* Prima se execută *clauza try* (adică, grupul de instrucțiuni situate
  între cuvintele-cheie "try" și "except").

* 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-cheie "except", se va executa *clauza except*,
  apoi execuția codului Python va trece la instrucțiunile situate după
  blocul try/except.

* If an exception occurs which does not match the exception named in
  the *except clause*, it is passed on to outer "try" statements; if
  no handler is found, it is an *unhandled exception* and execution
  stops with a message as shown above.

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

A class in an "except" clause is compatible with an exception if it is
the same class or a base class thereof (but not the other way around
--- an *except clause* listing a derived class is not compatible with
a base class). For example, the following code will print B, C, D in
that order:

   class B(Exception):
       pass

   class C(B):
       pass

   class D(C):
       pass

   for cls in [B, C, D]:
       try:
           raise cls()
       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.

All exceptions inherit from "BaseException", and so it can be used to
serve as a wildcard. Use this with extreme caution, since it is easy
to mask a real programming error in this way!  It can also be used to
print an error message and then re-raise the exception (allowing a
caller to handle the exception as well):

   import sys

   try:
       f = open('myfile.txt')
       s = f.readline()
       i = int(s.strip())
   except OSError as err:
       print("OS error: {0}".format(err))
   except ValueError:
       print("Could not convert data to an integer.")
   except BaseException as err:
       print(f"Unexpected {err=}, {type(err)=}")
       raise

Alternatively the last except clause may omit the exception name(s),
however the exception value must then be retrieved from
"sys.exc_info()[1]".

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 arg in sys.argv[1:]:
       try:
           f = open(arg, 'r')
       except OSError:
           print('cannot open', arg)
       else:
           print(arg, 'has', len(f.readlines()), 'lines')
           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".

When an exception occurs, it may have an associated value, also known
as the exception's *argument*. The presence and type of the argument
depend on the exception type.

The *except clause* may specify a variable after the exception name.
The variable is bound to an exception instance with the arguments
stored in "instance.args".  For convenience, the exception instance
defines "__str__()" so the arguments can be printed directly without
having to reference ".args".  One may also instantiate an exception
first before raising it and add any attributes to it as desired.

   >>> try:
   ...     raise Exception('spam', 'eggs')
   ... except Exception as inst:
   ...     print(type(inst))    # the exception instance
   ...     print(inst.args)     # arguments stored in .args
   ...     print(inst)          # __str__ allows args to be printed directly,
   ...                          # but may be overridden in exception subclasses
   ...     x, y = inst.args     # unpack args
   ...     print('x =', x)
   ...     print('y =', y)
   ...
   <class 'Exception'>
   ('spam', 'eggs')
   ('spam', 'eggs')
   x = spam
   y = eggs

If an exception has arguments, they are printed as the last part
('detail') of the message for unhandled exceptions.

Exception handlers don't just handle exceptions if they occur
immediately in the *try clause*, but also if they occur inside
functions that are called (even indirectly) in the *try clause*. For
example:

   >>> def this_fails():
   ...     x = 1/0
   ...
   >>> try:
   ...     this_fails()
   ... except ZeroDivisionError as err:
   ...     print('Handling run-time error:', err)
   ...
   Handling run-time error: 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('HiThere')
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
   NameError: HiThere

The sole argument to "raise" indicates the exception to be raised.
This must be either an exception instance or an exception class (a
class that derives from "Exception").  If an exception class is
passed, it will be implicitly instantiated by calling its constructor
with no arguments:

   raise ValueError  # shorthand for '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('HiThere')
   ... except NameError:
   ...     print('An exception flew by!')
   ...     raise
   ...
   An exception flew by!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   NameError: HiThere


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("database.sqlite")
   ... except OSError:
   ...     raise RuntimeError("unable to handle error")
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

   During handling of the above exception, another exception occurred:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
   RuntimeError: unable to handle error

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":

   # exc must be exception instance or None.
   raise RuntimeError from exc

Această clauză ne poate fi de folos atunci când transformăm excepții.
Ca aici:

   >>> def func():
   ...     raise ConnectionError
   ...
   >>> try:
   ...     func()
   ... except ConnectionError as exc:
   ...     raise RuntimeError('Failed to open database') from exc
   ...
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
     File "<stdin>", line 2, in func
   ConnectionError

   The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
   RuntimeError: Failed to open database

Tot ea ne ajută să dezafectăm înlănțuirea automată de excepții, cu
ajutorul idiomului "from None":

   >>> try:
   ...     open('database.sqlite')
   ... except OSError:
   ...     raise RuntimeError from None
   ...
   Traceback (most recent call last):
     File "<stdin>", line 4, in <module>
   RuntimeError

Pentru mai multe informații privitoare la mecanismul înlănțuirii, vezi
Excepții predefinite.


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.

Many standard modules define their own exceptions to report errors
that may occur in functions they define.  More information on classes
is presented in chapter Clase.


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('Goodbye, world!')
   ...
   Goodbye, world!
   Traceback (most recent call last):
     File "<stdin>", line 2, in <module>
   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 clauzele "except". Presupunând
  că excepția nu va fi interceptată de niciuna din clauzele "except",
  ea va fi relansată după încheierea execuției clauzei "finally".

* Se poate ca vreo excepție să survină când este executată fie una din
  clauzele "except" fie clauza "else". Și aici, excepția va fi
  relansată după ce se va încheia executarea codului din clauza
  "finally".

* Dacă în clauza "finally" se execută vreuna din instrucțiunile
  "break", "continue" ori "return", eventualele excepții nu vor mai fi
  relansate.

* Atunci când, la execuția codului din instrucțiunea "try" întâlnim
  vreuna din instrucțiunile "break", "continue" sau "return", clauza
  "finally" va fi executată *chiar înainte* de execuția oricăreia din
  instrucțiunile "break", "continue" ori "return".

* Dacă clauza "finally" include vreo instrucțiune "return", atunci
  valoarea returnată va fi cea dată de instrucțiunea "return" a
  respectivei clauze "finally" și nu valoarea dată de eventuala
  instrucțiune "return" a clauzei "try".

Un exemplu:

   >>> def bool_return():
   ...     try:
   ...         return True
   ...     finally:
   ...         return False
   ...
   >>> bool_return()
   False

Alt exemplu, mai sofisticat:

   >>> def divide(x, y):
   ...     try:
   ...         result = x / y
   ...     except ZeroDivisionError:
   ...         print("division by zero!")
   ...     else:
   ...         print("result is", result)
   ...     finally:
   ...         print("executing finally clause")
   ...
   >>> divide(2, 1)
   result is 2.0
   executing finally clause
   >>> divide(2, 0)
   division by zero!
   executing finally clause
   >>> divide("2", "1")
   executing finally clause
   Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "<stdin>", line 3, in divide
   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 line in open("myfile.txt"):
       print(line, 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("myfile.txt") as f:
       for line in f:
           print(line, 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ă.
