Professionele foutafhandeling met Python

In deze zelfstudie leer je hoe je foutcondities in Python vanuit het oogpunt van het hele systeem aanpakt. Foutafhandeling is een kritisch aspect van ontwerp en kruist de laagste niveaus (soms de hardware) helemaal tot aan de eindgebruikers. Als u geen consistente strategie hebt, is uw systeem onbetrouwbaar, is de gebruikerservaring slecht en heeft u veel problemen met het opsporen van fouten en het oplossen van problemen. 

De sleutel tot succes is zich bewust zijn van al deze in elkaar grijpende aspecten, deze expliciet in overweging nemend, en een oplossing vormen die elk punt behandelt.

Statuscodes versus uitzonderingen

Er zijn twee hoofdmodellen voor foutafhandeling: statuscodes en uitzonderingen. Statuscodes kunnen door elke programmeertaal worden gebruikt. Uitzonderingen vereisen ondersteuning voor taal / runtime. 

Python ondersteunt uitzonderingen. Python en zijn standaardbibliotheek gebruiken uitzonderingen royaal om te rapporteren over vele uitzonderlijke situaties zoals IO-fouten, delen door nul, indexering buiten de grenzen, en ook enkele niet zo uitzonderlijke situaties zoals einde van iteratie (hoewel het verborgen is). De meeste bibliotheken volgen en zoeken uitzonderingen op.

Dat betekent dat uw code hoe dan ook met de uitzonderingen van Python en bibliotheken moet omgaan, dus u kunt net zo goed uitzonderingen op uw code maken als dat nodig is en niet op statuscodes vertrouwen..

Snel voorbeeld

Voordat we in het innerlijk heiligdom van Python-uitzonderingen en praktische tips voor het verwerken van fouten duiken, laten we wat uitzonderingsafhandeling in actie zien:

def f (): return 4/0 def g (): verhoog Uitzondering ("Bel ons niet, we bellen je wel") def h (): probeer: f () behalve uitzondering als e: print (e) probeer: g () behalve uitzondering als e: print (e)

Dit is de uitvoer tijdens het bellen h ():

h () deling door nul Bel ons niet. We bellen je

Python-uitzonderingen

Python-uitzonderingen zijn objecten die zijn georganiseerd in een klassenhiërarchie. 

Hier is de hele hiërarchie:

BaseException + - SystemExit + - KeyboardInterrupt + - GeneratorExit + - Uitzondering + - StopIteration + - StandardError | + - BufferError | + - ArithmeticError | | + - FloatingPointError | | + - OverflowError | | + - ZeroDivisionError | + - AssertionError | + - AttributeError | + - EnvironmentError | | + - IOError | | + - OSError | | + - WindowsError (Windows) | | + - VMSError (VMS) | + - EOFError | + - ImportError | + - LookupError | | + - IndexError | | + - KeyError | + - MemoryError | + - NameError | | + - Niet geconsolideerdLocalError | + - ReferenceError | + - RuntimeError | | + - NotImplementedError | + - SyntaxError | | + - IndentationError | | + - TabError | + - SystemError | + - TypeError | + - ValueError | + - UnicodeError | + - UnicodeDecodeError | + - UnicodeEncodeError | + - UnicodeTranslateError + - Warning + - DeprecationWarning + - PendingDeprecationWarning + - RuntimeWarning + - SyntaxWarning + - UserWarning + - FutureWarning + - ImportWarning + - UnicodeWarning + - BytesWarning  

Er zijn verschillende speciale uitzonderingen die rechtstreeks van zijn afgeleid BaseException, net zoals SystemExit, KeyboardInterrupt en GeneratorExit. Dan is er de Uitzondering class, dat is de basisklasse voor StopIteration, Standaardfout en Waarschuwing. Alle standaardfouten zijn afgeleid van Standaardfout.

Wanneer u een uitzondering of een functie verhoogt die u hebt geroepen, wordt een uitzondering gegenereerd, wordt die normale codestroom beëindigd en de uitzondering begint de oproepstapel te verspreiden totdat deze een juiste uitzonderingshandler tegenkomt. Als er geen uitzonderingshandler beschikbaar is om het te verwerken, wordt het proces (of beter gezegd de huidige thread) beëindigd met een onverwerkte uitzonderingsboodschap.

Uitzonderingen verhogen

Uitzonderingen verhogen is heel eenvoudig. Je gebruikt gewoon de verhogen sleutelwoord om een ​​object te verhogen dat een subklasse van de is Uitzondering klasse. Het kan een voorbeeld zijn van Uitzondering zelf, een van de standaard uitzonderingen (bijv. RuntimeError), of een subklasse van Uitzondering je hebt jezelf afgeleid. Hier is een klein fragment dat alle gevallen laat zien:

# Een instantie van de klasse Exception zelf verhogen verhogen Uitzondering ('Ummm ... iets is fout') # Verhoog een instantie van de RuntimeError-klasse raise RuntimeError ('Ummm ... iets is fout') # Verhoog een aangepaste subklasse van Uitzondering die het tijdstempel bijhoudt de uitzondering is gemaakt vanuit datetime-import datetime-klasse SuperError (uitzondering): def __init __ (zelf, bericht): Exception .__ init __ (bericht) self.when = datetime.now () verhoogt SuperError ('Ummm ... iets is fout')

Uitzonderingen vangen

Je ziet uitzonderingen met de behalve clausule, zoals u in het voorbeeld hebt gezien. Wanneer u een uitzondering ontvangt, heeft u drie opties:

  • Slik het rustig (behandel het en blijf rennen).
  • Doe iets als loggen, maar hef dezelfde uitzondering opnieuw op om hogere niveaus te laten werken.
  • Breng een andere uitzondering in plaats van het origineel.

Slik de uitzondering door

U moet de uitzondering inslikken als u weet hoe u hiermee moet omgaan en volledig kunt herstellen. 

Als u bijvoorbeeld een invoerbestand ontvangt dat verschillende indelingen kan hebben (JSON, YAML), kunt u proberen het te parseren met verschillende parsers. Als de JSON-parser een uitzondering heeft gemaakt dat het bestand geen geldig JSON-bestand is, slikt u het en probeert u het met de YAML-parser. Als de YAML-parser ook is mislukt, kunt u de uitzondering uitbreiden.

import json importeer yaml def parse_file (bestandsnaam): probeer: return json.load (open (bestandsnaam)) behalve json.JSONDecodeError return yaml.load (open (bestandsnaam))

Merk op dat andere uitzonderingen (bijvoorbeeld bestand niet gevonden of geen leesrechten) zich zullen verspreiden en niet zullen worden gevangen door de specifieke uitzonderingsclausule. Dit is een goed beleid in dit geval waarbij u de YAML-parsing alleen wilt proberen als de JSON-parsering is mislukt vanwege een JSON-coderingsprobleem. 

Als je wilt omgaan allemaal uitzonderingen dan gewoon gebruiken behalve uitzondering. Bijvoorbeeld:

def print_exception_type (func, * args, ** kwargs): probeer: return func (* args, ** kwargs) behalve uitzondering als e: print type (e)

Merk op dat door toe te voegen als e, u bindt het uitzonderingsobject aan de naam e beschikbaar in uw uitzonderingsclausule.

Hernieuw dezelfde uitzondering

Om opnieuw te verhogen, voeg je gewoon toe verhogen zonder argumenten in je handler. Hiermee kun je wat lokale afhandeling uitvoeren, maar nog steeds kunnen de hogere niveaus hiermee omgaan. Hier de invoke_function () functie drukt het type uitzondering op de console af en verhoogt vervolgens de uitzondering.

def invoke_function (func, * args, ** kwargs): try: return func (* args, ** kwargs) behalve uitzondering als e: print type (e) raise

Verhoog een andere uitzondering

Er zijn verschillende gevallen waarin u een andere uitzondering zou willen opnemen. Soms wilt u meerdere verschillende low-level uitzonderingen groeperen in een enkele categorie die uniform wordt behandeld door code op een hoger niveau. In sommige gevallen moet u de uitzondering omzetten in gebruikersniveau en een toepassingsspecifieke context bieden. 

Eindelijk Clausule

Soms wilt u ervoor zorgen dat sommige opschoningscodes worden uitgevoerd, zelfs als ergens onderweg een uitzondering is opgetreden. U hebt bijvoorbeeld mogelijk een databaseverbinding die u wilt sluiten als u klaar bent. Hier is de verkeerde manier om het te doen:

def fetch_some_data (): db = open_db_connection () query (db) close_db_Connection (db)

Als het vraag () functie verhoogt een uitzondering dan de oproep naar close_db_connection () zal nooit worden uitgevoerd en de DB-verbinding blijft open. De Tenslotte clausule wordt altijd uitgevoerd nadat een try-alle-uitzonderingshandler is uitgevoerd. Hier is hoe het correct te doen:

def fetch_some_data (): db = Geen try: db = open_db_connection () query (db) tenslotte: als db niet None is: close_db_connection (db)

De oproep aan open_db_connection () mag geen verbinding retourneren of zelf een uitzondering maken. In dit geval is het niet nodig om de DB-verbinding te sluiten.

Tijdens gebruik Tenslotte, je moet oppassen dat je daar geen uitzonderingen maakt, omdat ze de oorspronkelijke uitzondering maskeren.

Contextmanagers

Contextmanagers bieden een ander mechanisme om resources zoals bestanden of DB-verbindingen in opschoningscode in te pakken die automatisch worden uitgevoerd, zelfs wanneer uitzonderingen zijn verheven. In plaats van try-finally blocks, gebruik je de met uitspraak. Hier is een voorbeeld met een bestand:

def process_file (bestandsnaam): met open (bestandsnaam) als f: process (f.read ()) 

Nu, zelfs als werkwijze() verhoogd een uitzondering, het bestand zal onmiddellijk worden gesloten wanneer de reikwijdte van de met blok wordt afgesloten, ongeacht of de uitzondering werd afgehandeld of niet.

logging

Loggen is vrijwel een vereiste in niet-triviale, langlopende systemen. Het is vooral handig in webtoepassingen waar u alle uitzonderingen op een generieke manier kunt behandelen: log gewoon de uitzondering in en stuur een foutmelding naar de beller. 

Bij het loggen is het handig om het uitzonderingstype, het foutbericht en de stacktrace te loggen. Al deze informatie is beschikbaar via de sys.exc_info object, maar als u de logger.exception () methode in uw uitzonderingsbehandelaar, zal het Python-loggingsysteem alle relevante informatie voor u extraheren.

Dit is de beste praktijk die ik aanbeveel:

import logging logger = logging.getLogger () def f (): try: flaky_func () behalve uitzondering: logger.exception () verhogen

Als je dit patroon volgt (ervan uitgaande dat je het loggen correct hebt ingesteld), wat er ook gebeurt, je hebt een redelijk goede staat van dienst in je logboeken van wat er mis is gegaan, en je zult het probleem kunnen oplossen.

Als je opnieuw verhoogt, zorg er dan voor dat je niet steeds dezelfde uitzondering op verschillende niveaus logt. Het is een verspilling en het kan u in de war brengen en u laten denken dat er meerdere instanties van hetzelfde probleem zijn opgetreden, terwijl in de praktijk een enkele instantie meerdere keren werd gelogd.

De eenvoudigste manier om dit te doen is om alle uitzonderingen te laten doorgeven (tenzij ze eerder met vertrouwen kunnen worden verwerkt en worden ingeslikt) en vervolgens de registratie dicht bij het hoogste niveau van uw toepassing / systeem te doen.

Schildwacht

Loggen is een mogelijkheid. De meest voorkomende implementatie is het gebruik van logbestanden. Maar voor grootschalige gedistribueerde systemen met honderden, duizenden of meer servers is dit niet altijd de beste oplossing. 

Om uitzonderingen over uw hele infrastructuur bij te houden, is een service als Sentry super handig. Het centraliseert alle uitzonderingsrapporten en voegt naast de stacktrace de status van elk stackframe toe (de waarde van variabelen op het moment dat de uitzondering werd verhoogd). Het biedt ook een hele mooie interface met dashboards, rapporten en manieren om berichten van meerdere projecten op te splitsen. Het is open source, dus je kunt je eigen server draaien of je abonneren op de gehoste versie.

Omgaan met transiënte mislukking

Sommige fouten zijn tijdelijk, vooral als het gaat om gedistribueerde systemen. Een systeem dat uitbarst bij het eerste teken van problemen is niet erg nuttig. 

Als uw code toegang zoekt tot een extern systeem dat niet reageert, is de traditionele oplossing time-out, maar soms is niet elk systeem voorzien van time-outs. Time-outs zijn niet altijd eenvoudig te kalibreren als de omstandigheden veranderen. 

Een andere benadering is om snel te falen en het opnieuw te proberen. Het voordeel is dat als het doelwit snel reageert, u niet veel tijd in de slaaptoestand hoeft door te brengen en onmiddellijk kunt reageren. Maar als het niet lukt, kunt u het meerdere keren proberen totdat u besluit dat het echt onbereikbaar is en een uitzondering genereert. In het volgende gedeelte zal ik een binnenhuisarchitect introduceren die het voor u kan doen.

Behulpzame decorateurs

Twee decorateurs die kunnen helpen met foutafhandeling zijn de @log_error, waarbij een uitzondering wordt geregistreerd en vervolgens opnieuw wordt verhoogd, en de @retry decorateur, die het opnieuw proberen een functie meerdere keren zal proberen.

Foutlogger

Hier is een eenvoudige implementatie. De decorateur behalve een logboekobject. Wanneer een functie wordt versierd en de functie wordt aangeroepen, wordt de aanroep verpakt in een try-except-component en als er een uitzondering is, wordt deze geregistreerd en wordt de uitzondering opnieuw verhoogd.

def log_error (logger) def ingericht (f): @ functools.wraps (f) def wrapped (* args, ** kwargs): try: return f (* args, ** kwargs) behalve uitzondering als e: if logger: logger .exception (e) verhoog retour gewikkeld retour versierd

Hier is hoe het te gebruiken:

import logging logger = logging.getLogger () @log_error (logger) def f (): verhoog Uitzondering ('Ik ben uitzonderlijk')

Retrier

Hier is een zeer goede implementatie van de @retry-decorateur.

importeren tijd importeren wiskunde # Retry-decorateur met exponentiële backoff-def. opnieuw proberen (probeert, delay = 3, backoff = 2): "Opnieuw een functie of methode totdat het True terugkeert. stelt de initiële vertraging in seconden in en backoff stelt de factor in waarmee de vertraging moet langer duren na elke fout, backoff moet groter zijn dan 1, anders is het niet echt een backoff.pogingen moeten minstens 0 zijn, en een vertraging groter dan 0. "als backoff <= 1: raise ValueError("backoff must be greater than 1") tries = math.floor(tries) if tries < 0: raise ValueError("tries must be 0 or greater") if delay <= 0: raise ValueError("delay must be greater than 0") def deco_retry(f): def f_retry(*args, **kwargs): mtries, mdelay = tries, delay # make mutable rv = f(*args, **kwargs) # first attempt while mtries > 0: als rv Waar is: # Gedaan op succes terug True mtries - = 1 # consumeren een poging time.sleep (mdelay) # wait ... mdelay * = backoff # make future wacht langer rv = f (* args, ** kwargs) # Probeer opnieuw return False # Ran out of tries :-( retour f_retry # true decorateur -> ingerichte functie return deco_retry # @retry (arg [, ...]) -> echte decorateur

Conclusie

Foutafhandeling is cruciaal voor zowel gebruikers als ontwikkelaars. Python biedt geweldige ondersteuning in de taal- en standaardbibliotheek voor op uitzonderingen gebaseerde foutafhandeling. Door ijverig de best practices te volgen, kunt u dit vaak verwaarloosde aspect overwinnen.

Leer Python

Leer Python met onze complete python-handleiding, of je nu net begint of dat je een ervaren coder bent die op zoek is naar nieuwe vaardigheden.