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

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. Exceptions (Εξαιρέσεις)

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

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

Η τελευταία γραμμή του μηνύματος σφάλματος υποδεικνύει τι συνέβη. Οι εξαιρέσεις υπάρχουν σε διαφορετικούς τύπους και ο τύπος εκτυπώνεται ως μέρος του μηνύματος: οι τύποι στο παράδειγμα είναι 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 ολοκληρώνεται.

  • If an exception occurs during execution of the try clause, the rest of the clause is skipped. Then if its type matches the exception named after the except keyword, the except clause is executed, and then execution continues after the try statement.

  • 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.

A try statement may have more than one except clause, to specify handlers for different exceptions. At most one handler will be executed. Handlers only handle exceptions that occur in the corresponding try clause, not in other handlers of the same try statement. An except clause may name multiple exceptions as a parenthesized tuple, for example:

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

Note that if the except clauses were reversed (with except B first), it would have printed B, B, B — the first matching except clause is triggered.

The last except clause may omit the exception name(s), 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:
    print("Unexpected error:", sys.exc_info()[0])
    raise

The tryexcept statement has an optional else clause, which, when present, must follow all except clauses. It is useful for code that must be executed if the try clause does not raise an exception. For example:

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.

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

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

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

Εάν πρέπει να προσδιορίσετε εάν έχει εγγραφεί μια εξαίρεση, αλλά δεν σκοπεύετε να τη χειριστείτε, μια απλούστερη μορφή της δήλωσης 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>
NameError: HiThere

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

The raise statement allows an optional from which enables chaining exceptions. For example:

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

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

>>> def func():
...     raise IOError
...
>>> try:
...     func()
... except IOError 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
OSError

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

Exception chaining happens automatically when an exception is raised inside an except or finally section. Exception chaining can be disabled by using from None idiom:

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

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

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

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

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

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

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 Κλάσεις.

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

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

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

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

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

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

  • Εάν η πρόταση finally εκτελέσει μια πρόταση break, continue ή return, οι εξαιρέσεις δεν αυξάνονται εκ νέου.

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

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

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

>>> 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>
  File "<stdin>", line 3, in divide
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 είναι πάντα κλειστό, ακόμα και αν παρουσιάστηκε πρόβλημα κατά την επεξεργασία των γραμμών. Τα αντικείμενα που, όπως τα αρχεία παρέχουν προκαθορισμένες ενέργειες καθαρισμού θα το υποδεικνύουν στην τεκμηρίωση τους.