8. Σφάλματα και Εξαιρέσεις

Μέχρι τώρα τα μηνύματα σφαλμάτων (error messages) δεν ήταν περισσότερα από όσα αναφέρθηκαν, αλλά αν έχετε δοκιμάσει τα παραδείγματα, πιθανότατα έχετε δει μερικά. Υπάρχουν (τουλάχιστον) δύο διαφορετικά είδη σφαλμάτων: syntax errors (συντακτικά σφάλματα) και exceptions (εξαιρέσεις).

8.1. Syntax Errors (Συντακτικά Σφάλματα)

Τα syntax errors, γνωστά και ως parsing errors, είναι ίσως το πιο συνηθισμένο είδος παραπόνου που λαμβάνετε ενώ εξακολουθείτε να μαθαίνετε Python:

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax

Ο αναλυτής επαναλαμβάνει την παραβατική γραμμή και εμφανίζει μικρά βέλη που δείχνουν προς το μέρος όπου που εντοπίστηκε το σφάλμα. Σημειώστε ότι αυτό δεν είναι πάντα το μέρος που πρέπει να διορθωθεί. Στο παράδειγμα, το σφάλμα εντοπίζεται στη συνάρτηση print(), καθώς λείπει μια άνω και κάτω τελεία (':') ακριβώς πριν από αυτήν.

Το όνομα αρχείου (<stdin> στο παράδειγμά μας) και ο αριθμός γραμμής εκτυπώνονται, ώστε να να γνωρίζετε πού να ψάξετε σε περίπτωση που η είσοδος προήλθε από αρχείο.

8.2. Exceptions (Εξαιρέσεις)

Ακόμη και αν μια πρόταση ή μια έκφραση είναι συντακτικά σωστή, μπορεί να προκαλέσει σφάλμα όταν γίνεται προσπάθεια εκτέλεσης της. Τα σφάλματα που εντοπίζονται κατά την εκτέλεση ονομάζονται εξαιρέσεις και δεν είναι άνευ όρων μοιραία (fatal): σύντομα θα μάθετε πως να τα χειρίζεστε σε προγράμματα Python. Ωστόσο, οι περισσότερες εξαιρέσεις δεν αντιμετωπίζονται από προγράμματα και οδηγούν σε μηνύματα σφάλματος όπως φαίνεται εδώ:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    10 * (1/0)
          ~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    4 + spam*3
        ^^^^
NameError: name 'spam' 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

Η τελευταία γραμμή του μηνύματος σφάλματος υποδεικνύει τι συνέβη. Οι εξαιρέσεις υπάρχουν σε διαφορετικούς τύπους και ο τύπος εκτυπώνεται ως μέρος του μηνύματος: οι τύποι στο παράδειγμα είναι ZeroDivisionError, NameError και TypeError. Η συμβολοσειρά που εκτυπώνεται ως τύπος εξαίρεσης είναι όνομα της ενσωματωμένης εξαίρεσης που προέκυψε. Αυτό ισχύει για όλες τις ενσωματωμένες (built-in) εξαιρέσεις, αλλά δεν χρειάζεται να ισχύει για εξαιρέσεις που ορίζονται από το χρήστη (αν και είναι μια χρήσιμη σύμβαση). Οι standard εξαιρέσεις είναι ενσωματωμένα (built-in) αναγνωριστικά (όχι δεσμευμένες λέξεις-κλειδιά).

Η υπόλοιπη γραμμή παρέχει λεπτομέρειες με βάση τον τύπο της εξαίρεσης και το τι την προκάλεσε.

Το προηγούμενο μέρος του μηνύματος σφάλματος εμφανίζει το περιβάλλον όπου συνέβη η εξαίρεση, με τη μορφή ανίχνευσης στοίβας. Γενικά περιέχει μια στοίβα ανίχνευσης γραμμών πηγής∙ ωστόσο, δεν θα εμφανίζει γραμμές που διαβάζονται από standard είσοδο.

Το Built-in Exceptions παραθέτει τις ενσωματωμένες εξαιρέσεις και τις έννοιές τους.

8.3. Διαχείριση Εξαιρέσεων

Είναι δυνατό να γραφτεί κώδικας που χειρίζεται επιλεγμένες εξαιρέσεις. Κοιτάξτε το ακόλουθο παράδειγμα, το οποίο ζητά από τον χρήστη να εισάγει έναν έγκυρο ακέραιο αριθμό, αλλά επιτρέπει στον χρήστη να διακόψει το πρόγραμμα (χρησιμοποιώντας Control-C ή ό,τι υποστηρίζει το λειτουργικό σύστημα)· σημειώστε ότι μια διακοπή που δημιουργείται από τον χρήστη σηματοδοτείται κάνοντας raise την εξαίρεση KeyboardInterrupt.

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

Η δήλωση try λειτουργεί ως εξής.

  • Πρώτον, εκτελείται η try clause (η πρόταση(εις) μεταξύ των λέξεων-κλειδιών try and except).

  • Εάν δεν προκύψει εξαίρεση, η except clause παραλείπεται και η εκτέλεση της πρότασης try ολοκληρώνεται.

  • Εάν παρουσιαστεί μια εξαίρεση κατά την εκτέλεση της πρότασης try, η υπόλοιπη πρόταση παραλείπεται. Στη συνέχεια, εάν ο τύπος της ταιριάζει με την εξαίρεση που ονομάζεται από τη λέξη-κλειδί except, η except clause εκτελείται, και στη συνέχεια η εκτέλεση συνεχίζεται μετά το μπλοκ try/except.

  • Εάν προκύψει μια εξαίρεση που δεν ταιριάζει με την εξαίρεση που αναφέρεται στην except clause, μεταβιβάζεται σε εξωτερικές εντολές try · εάν δεν βρεθεί κανένας χειριστής, είναι μια unhandled exception και η εκτέλεση σταματά με μήνυμα σφάλματος.

Μια πρόταση try μπορεί να έχει περισσότερες από μία except clause, για να καθορίσει χειριστές για διαφορετικές εξαιρέσεις. Το πολύ ένας χειριστής θα εκτελεστεί. Οι χειριστές χειρίζονται μόνο εξαιρέσεις που εμφανίζονται στην αντίστοιχη try clause, όχι σε άλλους χειριστές της ίδιας πρότασης try. Μια except clause μπορεί να ονομάσει πολλαπλές εξαιρέσεις ως πλειάδα (tuple) σε παρένθεση, για παράδειγμα:

... except (RuntimeError, TypeError, NameError):
...     pass

Μια κλάση σε μια πρόταση except ταιριάζει με εξαιρέσεις που είναι στιγμιότυπα της ίδιας της κλάσης ή μιας από τις παραγόμενες κλάσεις της (αλλά όχι το αντίστροφο — μια except clause που παραθέτει μια παράγωγη κλάση δεν ταιριάζει με τις παρουσίες των βασικών της κλάσεων). Για παράδειγμα, ο ακόλουθος κώδικας θα εκτυπώσει τα B, C, D με αυτή τη σειρά:

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

Σημειώστε ότι εάν οι except clauses είχαν αντιστραφεί (με το except B πρώτα), θα είχε εκτυπωθεί B, B, B — ενεργοποιείται η πρώτη αντιστοίχιση except clause.

Όταν προκύπτει μια εξαίρεση, μπορεί να έχει συσχετισμένες τιμές, γνωστές και ως ορίσματα της εξαίρεσης. Η παρουσία και οι τύποι των ορισμάτων εξαρτώνται από τον τύπο εξαίρεσης.

Το except clause μπορεί να καθορίσει μια μεταβλητή μετά το όνομα της εξαίρεσης. Η μεταβλητή συνδέεται με το στιγμιότυπο της εξαίρεσης η οποία συνήθως έχει ένα χαρακτηριστικό args που αποθηκεύει τα ορίσματα. Για ευκολία, οι ενσωματωμένοι (builtin) τύποι εξαίρεσης ορίζουν __str__() για να εκτυπώσετε όλα τα ορίσματα χωρίς ρητή πρόσβαση στο .args.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception type
...     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

Η έξοδος της εξαίρεσης __str__() εκτυπώνεται ως το τελευταίο μέρος (“λεπτομέρεια”) του μηνύματος για μη χειριζόμενες εξαιρέσεις.

Η BaseException είναι η κοινή βασική κλάση όλων των εξαιρέσεων. Μια από τις υποκατηγορίες της, Exception, είναι η βασική κλάση όλων των μη μοιραίων εξαιρέσεων. Εξαιρέσεις που δεν είναι υποκλάσεις του Exception δεν αντιμετωπίζονται συνήθως, επειδή χρησιμοποιούνται για να υποδείξουν ότι το πρόγραμμα πρέπει να τερματιστεί. Περιλαμβάνουν το SystemExit το οποίο αυξάνεται από το sys.exit() και το KeyboardInterrupt το οποίο γίνεται raise όταν ο χρήστης επιθυμεί να διακόψει την εκτέλεση του προγράμματος.

Η Exception μπορεί να χρησιμοποιηθεί ως μπαλαντέρ που πιάνει (σχεδόν) τα πάντα. Ωστόσο, είναι καλή πρακτική να είμαστε όσο το δυνατόν πιο συγκεκριμένοι με τους τύπους εξαιρέσεων που σκοπεύουμε να χειριστούμε και να επιτρέπουμε τυχόν απροσδόκητες εξαιρέσεις που εξαπλώνονται.

Το πιο κοινό μοτίβο για το χειρισμό Exception είναι να εκτυπώσετε ή να καταγράψετε την εξαίρεση και στη συνέχεια να την επαναφέρετε (επιτρέποντας σε έναν καλούντα να χειριστεί και την εξαίρεση):

import sys

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

Η πρόταση tryexcept έχει ένα προαιρετικό else clause, το οποίο, όταν υπάρχει, πρέπει να ακολουθεί όλες τις except clauses. Είναι χρήσιμο για κώδικα που πρέπει να εκτελεστεί εάν το try clause δεν κάνει raise μια εξαίρεση. Για παράδειγμα:

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()

Η χρήση της πρότασης else είναι καλύτερη από την προσθήκη πρόσθετου κώδικα στην πρόταση try, επειδή αποφεύγει την κατά λάθος σύλληψη μιας εξαίρεσης που δεν προέκυψε από τον κώδικα που προστατεύεται από την πρόταση tryexcept.

Οι χειριστές εξαιρέσεων δεν χειρίζονται μόνο τις εξαιρέσεις που εμφανίζονται αμέσως στη try clause, αλλά και εκείνες που εμφανίζονται μέσα σε συναρτήσεις που καλούνται (ακόμη και έμμεσα) στην try clause. Για παράδειγμα:

>>> 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. Raising Εξαιρέσεων

Η δήλωση raise επιτρέπει στον προγραμματιστή να αναγκάσει να εμφανιστεί μια καθορισμένη εξαίρεση. Για παράδειγμα:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise NameError('HiThere')
NameError: HiThere

Το μοναδικό όρισμα στο raise υποδεικνύει την εξαίρεση που πρέπει να γίνει raise. Αυτή πρέπει να είναι είτε μια παρουσία εξαίρεσης ή μια εξαίρεση κλάση (μια κλάση που προέρχεται από BaseException, όπως Exception ή μία από τις υποκλάσεις της). Εάν περάσει μια κλάση εξαίρεσης, θα δημιουργηθεί σιωπηρά καλώντας τον constructor της χωρίς ορίσματα:

κάνει raise ένα ValueError # συντομογραφία για το 'raise ValueError()'

Εάν πρέπει να προσδιορίσετε εάν έχει εγγραφεί μια εξαίρεση, αλλά δεν σκοπεύετε να τη χειριστείτε, μια απλούστερη μορφή της δήλωσης raise σας επιτρέπει να κάνετε ξανά raise την εξαίρεση:

>>> 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>
    raise NameError('HiThere')
NameError: HiThere

8.5. Αλυσιδωτές Εξαιρέσεις

Εάν παρουσιαστεί μια μη χειριζόμενη (unhandled) εξαίρεση μέσα σε μια ενότητα except, θα επισυνάψει την εξαίρεση που θα χειριστεί και θα συμπεριληφθεί στο μήνυμα σφάλματος:

>>> try:
...     open("database.sqlite")
... except OSError:
...     raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    open("database.sqlite")
    ~~~~^^^^^^^^^^^^^^^^^^^
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>
    raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error

Για να υποδείξετε ότι μια εξαίρεση είναι άμεση συνέπεια μιας άλλης, η πρόταση raise επιτρέπει μια προαιρετική πρόταση from:

# Το exc πρέπει να είναι παράδειγμα εξαίρεσης ή None.
κάνει raise το RuntimeError από exc

Αυτό μπορεί να είναι χρήσιμο όταν μετασχηματίζεται εξαιρέσεις. Για παράδειγμα:

>>> 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>
    func()
    ~~~~^^
  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>
    raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database

Επιτρέπει επίσης την απενεργοποίηση της αυτόματης αλυσίδας εξαιρέσεων χρησιμοποιώντας from None idiom:

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

Για περισσότερες πληροφορίες σχετικά με την μηχανική αλυσίδων, δείτε Built-in Exceptions.

8.6. Εξαιρέσεις που καθορίζονται από το χρήστη

Τα προγράμματα μπορούν να ονομάσουν τις δικές τους εξαιρέσεις δημιουργώντας μια νέα κλάση εξαιρέσεων (δείτε Κλάσεις για περισσότερα σχετικά με τις κλάσεις Python). Οι εξαιρέσεις θα πρέπει συνήθως να προέρχονται από την κλάση Exception, είτε άμεσα είτε έμμεσα.

Μπορούν να οριστούν κλάσεις εξαίρεσης που κάνουν οτιδήποτε μπορεί να κάνει οποιαδήποτε άλλη κλάση, αλλά συνήθως διατηρούνται απλές, συχνά προσφέρουν μόνο έναν αριθμό χαρακτηριστικών που επιτρέπουν την εξαγωγή πληροφοριών σχετικά με το σφάλμα από τους χειριστές για την εξαίρεση.

Οι περισσότερες εξαιρέσεις ορίζονται με ονόματα που τελειώνουν σε «Error», παρόμοια με την ονομασία των τυπικών εξαιρέσεων.

Πολλά standard modules ορίζουν τις δικές τους εξαιρέσεις για την αναφορά σφαλμάτων που μπορεί να προκύψουν σε συναρτήσεις που ορίζουν.

8.7. Καθορισμός ενεργειών καθαρισμού

Η δήλωση try έχει μια άλλη προαιρετική πρόταση που προορίζεται να ορίσει ενέργειες καθαρισμού που πρέπει να εκτελεστούν υπό οποιεσδήποτε συνθήκες. Για παράδειγμα:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise KeyboardInterrupt
KeyboardInterrupt

Εάν υπάρχει μια πρόταση finally, η πρόταση finally θα εκτελεστεί ως η τελευταία εργασία πριν από την ολοκλήρωση της πρότασης try. Η πρόταση finally εκτελείται είτε όχι η πρόταση try παράγει μια εξαίρεση. Τα ακόλουθα σημεία συζητούν πιο περίπλοκες περιπτώσεις όταν εμφανίζεται μια εξαίρεση:

  • Εάν παρουσιαστεί μια εξαίρεση κατά την εκτέλεση της πρότασης try, η εξαίρεση μπορεί να αντιμετωπιστεί από μια πρόταση except, Εάν η εξαίρεση δεν αντιμετωπίζεται από μια πρόταση except, η εξαίρεση γίνεται ξανά raise μετά την εκτέλεση της πρότασης finally.

  • Μια εξαίρεση θα μπορούσε να προκύψει κατά την εκτέλεση μιας πρότασης except ή else. Και πάλι, η εξαίρεση τίθεται ξανά μετά την εκτέλεση της πρότασης finally.

  • Εάν η πρόταση finally εκτελέσει μια πρόταση break, continue ή return, οι εξαιρέσεις δεν γίνονται raise εκ νέου. Αυτό μπορεί να προκαλέσει σύγχυση και ως εκ τούτου αποθαρρύνεται. Από την έκδοση 3.14, ο μεταγλωττιστής εκπέμπει μια SyntaxWarning για αυτό (δείτε PEP 765).

  • Εάν η πρόταση try φτάσει σε μια δήλωση break, continue ή return, η πρόταση finally θα εκτελεστεί ακριβώς πριν από τα break, continue or return της εκτέλεσης της δήλωσης.

  • Εάν μια πρόταση finally περιλαμβάνει μια δήλωση return, η τιμή που επιστρέφεται θα είναι αυτή από την πρόταση finally της δήλωσης της return, και όχι η τιμή από τη δήλωση try της πρότασης return. Αυτό μπορεί να προκαλέσει σύγχυση και επομένως αποθαρρύνεται. Από την έκδοση 3.14, ο μεταγλωττιστής εκπέμπει ένα SyntaxWarning για αυτό (δείτε PEP 765).

Για παράδειγμα:

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

Ένα πιο περίπλοκο παράδειγμα:

>>> 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>
    divide("2", "1")
    ~~~~~~^^^^^^^^^^
  File "<stdin>", line 3, in divide
    result = x / y
             ~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Όπως μπορείτε να δείτε, η πρόταση finally εκτελείται σε οποιαδήποτε περίπτωση. Το TypeError που δημιουργείται με τη διαίρεση δύο συμβολοσειρών δεν χειρίζεται από την πρόταση except και επομένως γίνεται ξανά raise μετά την εκτέλεση του όρου finally.

Στις εφαρμογές του πραγματικού κόσμου, η πρόταση finally είναι χρήσιμη για την απελευθέρωση εξωτερικών πόρων (όπως αρχεία ή συνδέσεις δικτύου), ανεξάρτητα από το εάν η χρήση του πόρου ήταν επιτυχής.

8.8. Προκαθορισμένες ενέργειες καθαρισμού

Μερικά αντικείμενα ορίζουν τις τυπικές ενέργειες καθαρισμού που πρέπει να αναλαμβάνονται όταν το αντικείμενο δεν χρειάζεται πλέον, ανεξάρτητα από το εάν η λειτουργία που χρησιμοποιεί το αντικείμενο πέτυχε ή απέτυχε. Κοιτάξτε το ακόλουθο αντικείμενο, το οποίο προσπαθεί να ανοίξει ένα αρχείο και να εκτυπώσει τα περιεχόμενα του στην οθόνη.

for line in open("myfile.txt"):
    print(line, end="")

Το πρόβλημα με αυτόν τον κώδικα είναι ότι αφήνει το αρχείο ανοιχτό για απροσδιόριστο χρονικό διάστημα μετά την ολοκλήρωση της εκτέλεσης αυτού του τμήματος του κώδικα. Αυτό δεν είναι πρόβλημα σε απλά σενάρια, αλλά μπορεί να είναι πρόβλημα για μεγαλύτερες εφαρμογές. Η δήλωση with επιτρέπει σε αντικείμενα όπως αρχεία να χρησιμοποιούνται με τρόπο που διασφαλίζει ότι καθαρίζονται πάντα άμεσα και σωστά.

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

Μετά την εκτέλεση της πρότασης, το αρχείο f είναι πάντα κλειστό, ακόμα και αν παρουσιάστηκε πρόβλημα κατά την επεξεργασία των γραμμών. Τα αντικείμενα που, όπως τα αρχεία παρέχουν προκαθορισμένες ενέργειες καθαρισμού θα το υποδεικνύουν στην τεκμηρίωση τους.

8.9. Raising και Χειρισμός Πολλαπλών Άσχετων Εξαιρέσεων

Υπάρχουν περιπτώσεις όπου είναι απαραίτητο να αναφερθούν πολλές εξαιρέσεις που έχουν συμβεί. Αυτό συμβαίνει συχνά σε πλαίσια ταυτόχρονης χρήσης, όταν πολλές εργασίες μπορεί να έχουνε αποτύχει παράλληλα, αλλά υπάρχουν και άλλες περιπτώσεις χρήσης όπου είναι επιθυμητό να συνεχιστεί η εκτέλεση και η συλλογή πολλαπλών σφαλμάτων αντί να κάνει raise την πρώτη εξαίρεση.

Η ενσωματωμένη (builtin) ExceptionGroup αναδιπλώνει μια λίστα με παρουσίες εξαιρέσεων ώστε να μπορούν να αυξηθούν μαζί. Είναι μια εξαίρεση από μόνη της, επομένως μπορεί να συλληφθεί όπως κάθε άλλη εξαίρεση.

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

Χρησιμοποιώντας except* αντί για except, μπορούμε επιλεκτικά να χειριστούμε μόνο τις εξαιρέσεις στην ομάδα που αντιστοιχούν σε έναν συγκεκριμένο τύπο. Στο παρακάτω παράδειγμα, το οποίο δείχνει μια ένθετη ομάδα εξαιρέσεων, κάθε πρόταση except* εξάγει από τις εξαιρέσεις της ομάδας ενός συγκεκριμένου τύπου, ενώ αφήνει όλες τις άλλες εξαιρέσεις να διαδοθούν σε άλλες προτάσεις και τελικά να ξαναγίνουν raise.

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 2, in f
  |     raise ExceptionGroup(
  |     ...<12 lines>...
  |     )
  | ExceptionGroup: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2 (1 sub-exception)
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

Λάβετε υπόψη ότι οι εξαιρέσεις που είναι ένθετες σε μια ομάδα εξαιρέσεων πρέπει να είναι στιγμιότυπα, όχι τύποι. Αυτό συμβαίνει επειδή στην πράξη οι εξαιρέσεις θα ήταν συνήθως αυτές που έχουν ήδη αναφερθεί και καταγραφεί από το πρόγραμμα, σύμφωνα με το ακόλουθο μοτίβο:

>>> excs = []
... for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...    raise ExceptionGroup("Test Failures", excs)
...

8.10. Εμπλουτίζοντας τις Εξαιρέσεις με Σημειώσεις

Όταν δημιουργείται μια εξαίρεση προκειμένου να γίνει raise, συνήθως αρχικοποιείται με πληροφορίες που περιγράφουν το σφάλμα που έχει προκύψει. Υπάρχουν περιπτώσεις όπου είναι χρήσιμο να προστεθούν πληροφορίες μετά την σύλληψη της εξαίρεσης. Για το σκοπό αυτό, οι εξαιρέσεις έχουνε μια μέθοδο add_note(note) που δέχεται μια συμβολοσειρά και την προσθέτει στη λίστα σημειώσεων της εξαίρεσης. Η standard απόδοση παρακολούθησης περιλαμβάνει όλες τις σημειώσεις, με τη σειρά που προστέθηκα, μετά την εξαίρεση.

>>> try:
...     raise TypeError('bad type')
... except Exception as e:
...     e.add_note('Add some information')
...     e.add_note('Add some more information')
...     raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>

Για παράδειγμα, όταν συλλέγουμε εξαιρέσεις σε μια ομάδα εξαιρέσεων, μπορεί να θέλουμε να προσθέσουμε πληροφορίες περιβάλλοντος για τα μεμονωμένα σφάλματα. Στην συνέχεια κάθε εξαίρεση στην ομάδα έχει μια σημείωση που υποδεικνύει πότε έχει συμβεί αυτό το σφάλμα.

>>> def f():
...     raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
...     try:
...         f()
...     except Exception as e:
...         e.add_note(f'Happened in Iteration {i+1}')
...         excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     raise ExceptionGroup('We have some problems', excs)
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>