angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Maak uw eigen Python aangepaste uitzonderingsklassen op maat van uw toepassing.

Een goede uitzonderingsbehandeling geeft u een betere leesbaarheid van de code, maar vereist ook dat u heel goed nadenkt over wat u wilt doen.

17 juni 2020
In Python
post main image
https://unsplash.com/@surface

Het gebruik van uitzonderingen in Python ziet er gemakkelijk uit, maar dat is het niet. Waarschijnlijk moet u de uitzonderingen en de behandeling van uitzonderingen bestuderen voordat u een Python code schrijft, maar TL;DR. Er zijn voorbeelden op het internet, helaas zijn de meeste zeer triviaal. Hoe dan ook, ik heb dit onderzocht en kwam met wat code die ik dacht te delen met jullie. Laat een reactie achter als je suggesties hebt.

Wat is een fout en wat is een uitzondering, wat is het verschil? In Python wordt een uitzondering gemaakt als er tijdens de uitvoering van het programma een fout optreedt, bijvoorbeeld een ValueError, ZeroDivisionError.

De voordelen van uitzonderingen ten opzichte van statussen kunnen als volgt worden samengevat, zie ook onderstaande link 'Waarom is het beter om een uitzondering te maken dan een foutcode terug te geven?

  1. Uitzonderingen laat uw code schoon van alle controles die nodig zijn bij het testen van de status van elke oproep.
  2. Uitzonderingen laten u toe om de terugkeerwaarde van de functies te gebruiken voor de werkelijke waarden
  3. Het belangrijkste is: uitzonderingen kunnen niet genegeerd worden door niets te doen, terwijl de status terug kan gaan naar

Niet alle mensen zijn zo positief, maar ik ga hier geen oorlog beginnen.

Ergens in onze code kunnen we een exception handler hebben die bijvoorbeeld gebruikt kan worden om een database insert terug te rollen. We kunnen ook de uitzondering laten bubbelen naar de top van de code en een foutmelding geven aan de user.

Logging en parameters

Als er iets gebeurt, moeten we ervoor zorgen dat we de fout registreren. Om het lokaliseren en oplossen van een (codeer)probleem te vergemakkelijken zetten we zoveel mogelijk informatie in ons logboek. Ik heb besloten dat ik de volgende informatie in het logboek wil hebben:

  • een traceback
  • de volgende (optionele) parameters:
    • e
      Dit is de waarde die (vaak) door een uitzondering wordt geretourneerd
    • code
      Dit kan onze eigen foutcode zijn
    • bericht
      Dit is het bericht dat we willen laten zien aan de user (websitebezoeker, API user)
    • details
      Meer informatie die nuttig kan zijn voor de user, bijv. meegeleverde parameters
    • fargs
      Dit zijn de argumenten van de functie waar de uitzondering zich voordeed, handig voor het debuggen

Een voorbeeld

Stel dat we een applicatie maken die gebruik maakt van een databaseklasse en een imap-serverklasse. We maken onze aangepaste Exception handler. Het is een goede gewoonte om het in een bestand exceptions.py te hebben dat we importeren in onze applicatie:

# file: exceptions.py

class AppError(Exception):

    def __init__(self, e=None, code=None, message=None, details=None, fargs=None):
        self.e = e
        self.code = code
        self.message = message
        self.details = details
        self.fargs = fargs

    def get_e(self):
        return self.e

    def get_code(self):
        return self.code

    def get_message(self):
        return self.message

    def get_details(self):
        return self.details

    def __str__(self):
        s_items = []
        if self.e is not  None:
            s_items.append('e = {}'.format(self.e))
        if self.code is not  None:
            s_items.append('code = {}'.format(self.code))
        if self.message is not  None:
            s_items.append('message = {}'.format(self.message))
        if self.details is not  None:
            s_items.append('details = {}'.format(self.details))
        if self.fargs is not  None:
            s_items.append('fargs = {}'.format(self.fargs))
        return ', '.join(s_items)


class DbError(AppError):
    pass


class IMAPServerError(AppError):
    pass

Dit lijkt erg op het maken van normale klassen. In dit geval geven we nul of meer genoemde argumenten door aan de uitzondering. We hebben de __str__() methode nodig om Python een string met beschikbare data terug te laten sturen die in het app.log bestand wordt opgeslagen. Als we __str__() weglaten dan wordt app.log alleen weergegeven: exceptions.IMAPServerError'.

De app code:

# file: app.py
 
from exceptions import AppError, DbError, IMAPServerError

import logging

logging.basicConfig(
    format='%(asctime)s - %(levelname)s: %(message)s',
    filename='app.log',
    level=logging.DEBUG
    #level=logging.INFO
)

class A:

    def do_a(self, a):
        # connect to remote system
        b = B()
        b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })

        # do something else


class B:

    def do_b(self, a, b=None, c=None):
        fname = 'B.do_b'
        fargs = locals()

        abc = 'not in locals'

        # check values
        if b not in [8, 16]:
            raise IMAPServerError(
                fargs = fargs,
                e = 'Input must be 8 or 16',
                message = 'Input must be 8 or 16, value supplied: {}'.format(b)
            )

        # connect to remote system
        try:
            # simulate something went wrong
            # raise IMAPServerError('error 123')
            if b == 8:
                d = b/0

        except (ZeroDivisionError, IMAPServerError) as e:
            raise IMAPServerError(
                fargs = fargs,
                e = e,
                message = 'Connection to remote system failed. Please check your settings',
                details = 'Connection parameters:  username = John'
            ) from e


def run(i):

    a = A()
    a.do_a(i)


def do_run():

    # 7: input error
    # 8: connection error
    # 16: no error
    for i in [7, 8, 16]:

        print('Run with i = {}'.format(i))
        try:
            run(i)
            print('No error(s)')

        except (DbError, IMAPServerError) as e:
            logging.exception('Stack trace')
            print('Error: {}'.format(e.message))
            print('Details: {}'.format(e.details))

    print('Ready')


if __name__ == '__main__':
    do_run()

Merk op dat uitzonderingen worden gelogd met behulp van 'logging.exception' op een bestand app.log. Ook gebruiken we de Python locals() functie om de argumenten van de functie of methode waar de uitzondering zich voordeed vast te leggen. In de 'connect to remote system'-uitzondering verhogen we met alle parameters die we in het logboek willen hebben. Tot slot gebruiken we hier 'raise ... from e' construct. Dit onthult de ZeroDivisionError als de oorspronkelijke oorzaak.

Om te draaien, typ:

python3 app.py

Het resultaat is:

Run with i = 7
Error: Input must be 8 or 16, value supplied: 7
Details:  None
Run with i = 8
Error: Connection to remote system failed. Please check your settings
Details: Connection parameters:  username = John
Run with i = 16
No error(s)
Ready

en het logboekbestand app.log:

2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
  File "app.py", line 71, in do_run
    run(i)
  File "app.py", line 59, in run
    a.do_a(i)
  File "app.py", line 19, in do_a
    b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
  File "app.py", line 38, in do_b
    message = 'Input must be 8 or 16, value supplied: {}'.format(b)
exceptions.IMAPServerError: e = Input must be 8 or 16, message = Input must be 8 or 16, value supplied: 7, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 7, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}
2020-06-17 15:29:41,939 - ERROR: Stack trace
Traceback (most recent call last):
  File "app.py", line 46, in do_b
    d = b/0
ZeroDivisionError: division by zero

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

Traceback (most recent call last):
  File "app.py", line 71, in do_run
    run(i)
  File "app.py", line 59, in run
    a.do_a(i)
  File "app.py", line 19, in do_a
    b.do_b('unnamed parameter', b=a, c={ 'one': 'first', 'two': 'second' })
  File "app.py", line 53, in do_b
    ) from e
exceptions.IMAPServerError: e = division by zero, message = Connection to remote system failed. Please check your settings, details = Connection parameters:  username = John, fargs = {'fname': 'B.do_b', 'c': {'one': 'first', 'two': 'second'}, 'b': 8, 'a': 'unnamed parameter', 'self': <__main__.B object at 0x7f20aab66748>}

app.log bevat twee ERROR-items. Een voor de 'Input must be 8 of 16' fout en een voor de 'Connection to remote system failed' fout.

Samenvatting

Het leven zou eenvoudig kunnen zijn, maar dat is het niet. Een goede uitzonderingsbehandeling zorgt ervoor dat u de code gemakkelijker kunt lezen, maar vereist ook dat u heel goed nadenkt over wat u wilt doen. Het zou mooi zijn als we ook de argumenten van de aangeroepen functies kunnen toevoegen die leiden tot de functie die de uitzondering opheft, maar dit is moeilijker.

Links / credits

How to get value of arguments passed to functions on the stack?
https://stackoverflow.com/questions/6061744/how-to-get-value-of-arguments-passed-to-functions-on-the-stack

Proper way to declare custom exceptions in modern Python?
https://stackoverflow.com/questions/1319615/proper-way-to-declare-custom-exceptions-in-modern-python

The Most Diabolical Python Antipattern
https://realpython.com/the-most-diabolical-python-antipattern/

Why Exceptions Suck (ckwop.me.uk)
https://news.ycombinator.com/item?id=232890

Why is it better to throw an exception rather than return an error code?
https://stackoverflow.com/questions/4670987/why-is-it-better-to-throw-an-exception-rather-than-return-an-error-code

Your API sucks (so I’ll catch all exceptions)
https://dorinlazar.ro/your-api-sucks-catch-exceptions/

Lees meer

Exceptions

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.