Consemnarea excepțională a excepțiilor în Python

Aaron Maxwell este autorul cărții Powerful Python.

Excepțiile se întâmplă. Și, ca dezvoltatori, pur și simplu trebuie să ne ocupăm de ele. Chiar și atunci când scriem software care să ne ajute să găsim burrito.

Așteptați, mă grăbesc… vom reveni la asta. După cum spuneam: Modul în care ne ocupăm de excepții depinde de limbaj. Iar pentru software-ul care funcționează la scară largă, jurnalizarea este unul dintre cele mai puternice și mai valoroase instrumente pe care le avem pentru a face față condițiilor de eroare. Să ne uităm la câteva moduri în care acestea funcționează împreună.

Podul „Big Tarp”

Vom începe de la o extremă:

try: main_loop()except Exception: logger.exception("Fatal error in main loop")

Aceasta este o viziune generală. Este potrivit pentru anumite trasee de cod în care știți că blocul de cod (de exemplu, main_loop()) poate ridica un număr de excepții pe care poate nu le anticipați. Și mai degrabă decât să permiteți ca programul să se încheie, decideți că este preferabil să înregistrați informațiile de eroare și să continuați de acolo.

Magia aici este cu metoda exception. (logger este obiectul logger al aplicației dumneavoastră – ceva care a fost returnat de la logging.getLogger(), de exemplu). Această metodă minunată captează urmărirea completă a stivei în contextul blocului except, și o scrie în întregime.

Rețineți că nu trebuie să treceți obiectul excepției aici. Trebuie să treceți un șir de mesaje. Acest lucru va consemna urmărirea completă a stivei, dar va prelungi o linie cu mesajul dvs. Astfel, mesajul cu mai multe linii care apare în jurnalul dvs. ar putea arăta astfel:

Fatal error in main loopTraceback (most recent call last): File "bigtarp.py", line 14, in main_loop() File "bigtarp.py", line 9, in main_loop print(foo(x)) File "bigtarp.py", line 4, in foo return 10 // n ZeroDivisionError: integer division or modulo by zero

Detalii urmăririi stivei nu contează – acesta este un exemplu de jucărie care ilustrează o soluție adultă la o problemă din lumea reală. Observați doar că prima linie este mesajul pe care l-ați transmis la logger.exception(), iar liniile următoare sunt urmărirea completă a stivei, inclusiv tipul de excepție (ZeroDivisionError în acest caz). Va prinde și înregistra orice tip de eroare în acest mod.

În mod implicit, logger.exception utilizează nivelul de înregistrare ERROR. Alternativ, puteți utiliza metodele obișnuite de logare – logger.debug(), logger.info(), logger.warn(), etc. – și să treceți parametrul exc_info, setându-l la True:

while True: try: main_loop() except Exception: logger.error("Fatal error in main loop", exc_info=True)

Setând exc_info la True, logarea va face ca logarea să includă urmărirea completă a stivei…. exact cum face logger.exception(). Singura diferență este că puteți schimba cu ușurință nivelul de jurnalizare la altceva decât eroare: Pur și simplu înlocuiți logger.error cu logger.warn, de exemplu.

Fapt amuzant: Modelul Big Tarp are un corespondent aproape diabolic, despre care veți citi mai jos.

Modelul „Pinpoint”

Acum să ne uităm la cealaltă extremă. Imaginați-vă că lucrați cu OpenBurrito SDK, o bibliotecă care rezolvă problema crucială de a găsi un local de burrito târziu în noapte în apropierea locației dvs. curente. Să presupunem că are o funcție numită find_burrito_joints() care, în mod normal, returnează o listă de restaurante potrivite. Dar, în anumite circumstanțe rare, poate ridica o excepție numită BurritoCriteriaConflict.

from openburrito import find_burrito_joints, BurritoCriteriaConflict# "criteria" is an object defining the kind of burritos you want.try: places = find_burrito_joints(criteria)except BurritoCriteriaConflict as err: logger.warn("Cannot resolve conflicting burrito criteria: {}".format(err.message)) places = list()

Patronul aici este de a executa în mod optimist un cod – apelul la find_burrito_joints(), în acest caz – în cadrul unui bloc de încercări. În cazul în care se ridică un anumit tip de excepție, se înregistrează un mesaj, se rezolvă situația și se merge mai departe.

Diferența cheie este clauza except. Cu Big Tarp, practic prindeți și înregistrați orice excepție posibilă. Cu Pinpoint, prindeți un tip de excepție foarte specific, care are relevanță semantică în acel loc anume din cod.

Rețineți, de asemenea, că eu folosesc logger.warn() în loc de logger.exception(). (În acest articol, ori de câte ori vedeți warn(), puteți înlocui info(), sau error(), etc.) Cu alte cuvinte, înregistrez un mesaj la o anumită severitate în loc să înregistrez întreaga urmă de stivă.

De ce arunc informația de urmărire a stivei? Pentru că nu este la fel de utilă în acest context, în care surprind un anumit tip de excepție, care are o semnificație clară în logica codului. De exemplu, în acest fragment:

characters = {"hero": "Homer", "villain": "Mr. Burns"}# Insert some code here that may or may not add a key called# "sidekick" to the characters dictionary.try: sidekick = charactersexcept KeyError: sidekick = "Milhouse"

Aici, KeyError nu este o eroare oarecare. Atunci când este ridicată, înseamnă că a apărut o situație specifică – și anume, nu există un rol de „sidekick” definit în distribuția mea de personaje, așa că trebuie să revin la un rol implicit. Umplerea jurnalului cu o urmă de stivă nu va fi utilă în acest tip de situație. Și aici veți folosi Pinpoint.

Programul „Transformer”

Aici, prindeți o excepție, o înregistrați, apoi ridicați o altă excepție. În primul rând, iată cum funcționează în Python 3:

try: something()except SomeError as err: logger.warn("...") raise DifferentError() from err

În Python 2, trebuie să renunțați la „from err”:

try: something()except SomeError as err: logger.warn("...") raise DifferentError()

(Acest lucru se dovedește a avea implicații mari. Mai multe despre asta într-un moment.) Veți dori să folosiți acest model atunci când poate fi ridicată o excepție care nu se potrivește bine cu logica aplicației dumneavoastră. Acest lucru se întâmplă adesea în jurul limitelor bibliotecii.

De exemplu, imaginați-vă că folosiți SDK-ul openburrito pentru aplicația dvs. ucigașă care le permite oamenilor să găsească localuri de burrito până târziu în noapte. Funcția find_burrito_joints() poate ridica BurritoCriteriaConflict dacă suntem prea pretențioși. Acesta este API-ul expus de SDK, dar nu se potrivește în mod convenabil cu logica de nivel superior a aplicației dumneavoastră. O potrivire mai bună în acest punct al codului este o excepție pe care ați definit-o, numită NoMatchingRestaurants.

În această situație, veți aplica modelul astfel (pentru Python 3):

from myexceptions import NoMatchingRestaurantstry: places = find_burrito_joints(criteria)except BurritoCriteriaConflict as err: logger.warn("Cannot resolve conflicting burrito criteria: {}".format(err.message)) raise NoMatchingRestaurants(criteria) from err

Acest lucru cauzează o singură linie de ieșire în jurnalul dvs. și declanșează o nouă excepție. Dacă nu este prinsă niciodată, ieșirea de eroare a acestei excepții arată astfel:

Traceback (most recent call last): File "transformerB3.py", line 8, in places = find_burrito_joints(criteria) File "/Users/amax/python-exception-logging-patterns/openburrito.py", line 7, in find_burrito_joints raise BurritoCriteriaConflictopenburrito.BurritoCriteriaConflictThe above exception was the direct cause of the following exception:Traceback (most recent call last): File "transformerB3.py", line 11, in raise NoMatchingRestaurants(criteria) from errmyexceptions.NoMatchingRestaurants: {'region': 'Chiapas'}

Acum acest lucru este interesant. Ieșirea include urmărirea stivei pentru NoMatchingRestaurants. Și raportează și BurritoCriteriaConflict care a instigat-o… specificând în mod clar care a fost cea originală.

În Python 3, excepțiile pot fi acum înlănțuite. Sintaxa raise ... from ... oferă acest lucru. Când spuneți raise NoMatchingRestaurants(criteria) from err, asta ridică o excepție de typeNoMatchingRestaurants. Această excepție ridicată are un atribut numit __cause__, a cărui valoare este excepția instigatoare. Python 3 folosește acest lucru pe plan intern atunci când raportează informațiile despre eroare.

Cum se face acest lucru în Python 2? Ei bine, nu se poate. Aceasta este una dintre acele bunătăți pe care trebuie doar să le actualizați pentru a le obține. În Python 2, sintaxa „raise … from” nu este suportată, astfel încât ieșirea dvs. de excepție va include doar urmă de stivă pentru NoMatchingRestaurants. Modelul Transformer este în continuare perfect util, bineînțeles.

Modelul „Message and Raise”

În acest model, înregistrați că o excepție apare într-un anumit punct, dar apoi permiteți ca aceasta să se propage și să fie tratată la un nivel superior:

try: something()except SomeError: logger.warn("...") raise

Nu tratați efectiv excepția. Pur și simplu întrerupeți temporar fluxul pentru a înregistra un eveniment. Veți face acest lucru atunci când aveți în mod specific un gestionar de nivel superior pentru eroare și doriți să vă întoarceți la acesta, dar doriți, de asemenea, să înregistrați faptul că eroarea a apărut, sau semnificația acesteia, într-un anumit loc din cod.

Acest lucru poate fi cel mai util în depanare – atunci când primiți o excepție, dar încercați să înțelegeți mai bine contextul de apelare. Puteți interjecta această declarație de logare pentru a furniza informații utile și chiar să o implementați în siguranță în producție dacă aveți nevoie să observați rezultatele în condiții realiste.

Anti-patternul „mesajului criptic”

Acum ne vom îndrepta atenția asupra unor anti-patternuri… lucruri pe care nu ar trebui să le faceți în codul dumneavoastră.

try: something()except Exception: logger.error("...")

Să presupunem că dumneavoastră sau cineva din echipa dumneavoastră scrie acest cod, iar apoi, șase luni mai târziu, vedeți un mesaj ciudat în jurnalul dumneavoastră. Ceva de genul:

ERROR: something bad happened

Acum sper și mă rog să nu vedeți cuvintele „ceva rău s-a întâmplat” în jurnalul dvs. real. Cu toate acestea, textul real al jurnalului pe care îl vedeți poate fi la fel de derutant. Ce trebuie să faceți în continuare?

Bine, primul lucru este să vă dați seama unde în baza de cod este înregistrat acest mesaj vag. Dacă sunteți norocos, veți putea să faceți rapid grep prin cod și să găsiți exact o singură posibilitate. Dacă nu sunteți norocos, este posibil să găsiți mesajul de logare în mai multe căi de cod complet diferite. Ceea ce vă va lăsa cu mai multe întrebări:

  • Care dintre ele declanșează eroarea?
  • Sau este vorba de mai multe dintre ele? Sau TOATE?
  • Ce intrare din jurnal corespunde cărui loc?

Câteodată, însă, nici măcar nu este posibil să faci grep sau să cauți prin cod pentru a afla unde se întâmplă, pentru că se generează textul din jurnal. Luați în considerare:

 what = "something" quality = "bad" event = "happened" logger.error("%s %s %s", what, quality, event)

Cum ați putea chiar să căutați asta? Este posibil să nici nu vă gândiți la asta, cu excepția cazului în care ați căutat mai întâi fraza completă și nu ați obținut niciun rezultat. Și dacă ați obținut o potrivire, ar putea fi cu ușurință un fals pozitiv.

Soluția ideală este să treceți argumentul exc_info:

try: something()except Exception: logger.error("something bad happened", exc_info=True)

Când faceți acest lucru, o urmă de stivă completă este inclusă în jurnalele aplicației. Acest lucru vă spune exact ce linie din ce fișier cauzează problema, cine a invocat-o, etc… toate informațiile de care aveți nevoie pentru a începe depanarea.

The Most Diabolical Python Antipattern

Dacă vă văd vreodată făcând asta, voi veni la voi acasă pentru a vă confisca computerele, apoi vă voi sparge contul github și vă voi șterge toate depozitele:

try: something()except Exception: pass

În buletinul meu informativ, mă refer la acest lucru ca fiind „The Most Diabolical Python Antipattern”. Observați cum acest lucru nu numai că nu reușește să vă ofere nicio informație utilă despre excepții. De asemenea, reușește să ascundă complet faptul că ceva este greșit în primul rând. S-ar putea să nu știți niciodată că ați tastat greșit un nume de variabilă – da, acest lucru maschează de fapt NameError – până când sunteți chemat pe pager la ora 2 dimineața pentru că producția este stricată, într-un mod care vă ia până în zori să vă dați seama, deoarece toate informațiile posibile de depanare sunt suprimate.

Simplu, nu o faceți. Dacă simțiți că pur și simplu trebuie să prindeți și să ignorați toate erorile, cel puțin aruncați o prelată mare sub ea (adică folosiți logger.exception() în loc de pass).

Mai multe modele de înregistrare a excepțiilor

Există mult mai multe modele pentru înregistrarea informațiilor despre excepții în Python, cu diferite compromisuri, argumente pro și contra. Ce modele ați găsit utile, sau nu? Anunțați pe toată lumea în comentarii.

Învățați mai multe despre Bazele logării Python.

Mărcile comerciale, mărcile de serviciu și logo-urile Loggly și SolarWinds sunt proprietatea exclusivă a SolarWinds Worldwide, LLC sau a afiliaților săi. Toate celelalte mărci comerciale sunt proprietatea proprietarilor lor respectivi.

.

Lasă un răspuns

Adresa ta de email nu va fi publicată.