Μια επισκόπηση του πώς να διαβάζεις tracebacks της Python για αποσφαλμάτωση.
Όταν καλείται μια συνάρτηση, δημιουργείται ένα αντικείμενο frame για να κρατήσει τις τοπικές μεταβλητές και τα ορίσματα που περνούν στη συνάρτηση.
Όταν η συνάρτηση επιστρέφει, το αντικείμενο frame καταστρέφεται.
Όταν η συνάρτηση B καλείται μέσα στη συνάρτηση A, οι τιμές της συνάρτησης B τοποθετούνται σε ένα αντικείμενο frame, το οποίο μετά μπαίνει πάνω από το αντικείμενο frame της συνάρτησης A στη στοίβα κλήσεων.
Η στοίβα κλήσεων είναι μια συλλογή από αντικείμενα frame για τις συναρτήσεις που είναι ενεργές εκείνη τη στιγμή.
Αν η συνάρτηση A έχει καλέσει τη συνάρτηση B και η συνάρτηση B έχει καλέσει τη συνάρτηση C, τότε τα αντικείμενα frame και των τριών συναρτήσεων θα βρίσκονται στη στοίβα κλήσεων.
Μόλις επιστρέψει η συνάρτηση C, το αντικείμενο frame της θα αφαιρεθεί από τη στοίβα και μόνο τα αντικείμενα frame των συναρτήσεων A και B θα παραμείνουν στη στοίβα κλήσεων.
Ένα traceback είναι μια αναφορά όλων των αντικειμένων frame που βρίσκονται στη στοίβα σε μια δεδομένη στιγμή. Όταν ένα πρόγραμμα Python συναντήσει μια εξαίρεση που δεν έχει χειριστεί, θα τυπώσει το μήνυμα της εξαίρεσης και ένα traceback. Το traceback θα δείξει πού πετάχτηκε η εξαίρεση και ποιες συναρτήσεις κλήθηκαν μέχρι να φτάσουμε σε αυτήν.
Το ValueError είναι μια συνηθισμένη εξαίρεση.
Το παρακάτω είναι ένα παράδειγμα ValueError που προκύπτει όταν προσπαθείς να αντιστοιχίσεις δύο μεταβλητές στα αριστερά από μία μόνο τιμή στα δεξιά:
>>> first, second = [1]
Traceback (most recent call last):
File <stdin>, line 1, in <module>
first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)
Τα tracebacks οργανώνονται με την πιο πρόσφατη κλήση στο τέλος, οπότε το σημείο από όπου ξεκινάς να διαβάζεις το traceback είναι η εξαίρεση στο κάτω μέρος. Διαβάζοντας προς τα πάνω από εκεί, βλέπεις πώς η εκτέλεση έφτασε σε αυτή την εντολή. Αν βάλεις την προβληματική γραμμή μέσα σε μια συνάρτηση και μετά καλέσεις τη συνάρτηση, θα δεις ένα μεγαλύτερο trace:
>>> def my_func():
... first, second = [1]
...
>>> my_func()
Traceback (most recent call last):
File <stdin>, line 5, in <module>
my_func()
File <stdin>, line 2, in my_func
first, second = [1]
ValueError: not enough values to unpack (expected 2, got 1)
Κοιτώντας ανάποδα από το κάτω μέρος, βλέπεις ότι η κλήση όπου συνέβη η εξαίρεση βρίσκεται στη γραμμή 2 μέσα στη my_func.
Έφτασες εκεί καλώντας τη my_func στη γραμμή 5.
Η Python ορίζει πάνω από 60 ενσωματωμένες κλάσεις εξαιρέσεων. Ακολουθεί μια σύντομη επισκόπηση μερικών από τις πιο συνηθισμένες εξαιρέσεις και το τι σημαίνουν.
Η Python πετάει ένα SyntaxError όταν δεν μπορεί να καταλάβει τον κώδικα λόγω μη έγκυρης σύνταξης.
Για παράδειγμα, μπορεί να υπάρχει μια ανοιχτή παρένθεση χωρίς την αντίστοιχη κλειστή παρένθεση.
Τρέχοντας αυτόν τον κώδικα:
def distance(strand_a, strand_b):
if len(strand_a) != len(strand_b):
raise ValueError("Strands must be of equal length." # This is missing the closing parenthesis
θα έχει ως αποτέλεσμα ένα stack trace παρόμοιο με αυτό (πρόσεξε το μήνυμα στην τελευταία γραμμή):
.usr.local.lib.python3.10.site-packages._pytest.python.py:608: in _importtestmodule
mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
.usr.local.lib.python3.10.site-packages._pytest.pathlib.py:533: in import_path
importlib.import_module(module_name)
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1050: in _gcd_import ???
<frozen importlib._bootstrap>:1027: in _find_and_load ???
<frozen importlib._bootstrap>:1006: in _find_and_load_unlocked ???
<frozen importlib._bootstrap>:688: in _load_unlocked ???
.usr.local.lib.python3.10.site-packages._pytest.assertion.rewrite.py:168: in exec_module
exec(co, module.__dict__)
.mnt.exercism-iteration.hamming_test.py:3: in <module>
from hamming import (
E File ".mnt.exercism-iteration.hamming.py", line 10
E raise ValueError("Strands must be of equal length."
E ^
E SyntaxError: '(' was never closed
Η Python πετάει ένα AssertionError όταν αποτύχει μια εντολή assert (δες παρακάτω).
Τρέχοντας αυτόν τον κώδικα:
def distance(strand_a, strand_b):
assert len(strand_a) == len(strand_b)
distance("ab", "abc")
θα έχει ως αποτέλεσμα ένα stack trace παρόμοιο με αυτό (πρόσεξε το μήνυμα στην τελευταία γραμμή):
hamming_test.py:3: in <module>
from hamming import (
hamming.py:5: in <module>
distance("ab", "abc")
hamming.py:2: in distance
assert len(strand_a) == len(strand_b)
E AssertionError
Ένα AttributeError πετιέται όταν ο κώδικας (ή ένα unit test!) προσπαθεί να προσπελάσει ένα χαρακτηριστικό ενός αντικειμένου, αλλά το αντικείμενο δεν έχει τέτοιο χαρακτηριστικό.
Για παράδειγμα, ένα unit test περιμένει ένα αντικείμενο Robot να έχει ένα χαρακτηριστικό direction, αλλά όταν προσπάθησε να προσπελάσει το robot.direction, αυτό δεν υπάρχει.
Αυτό μπορεί επίσης να υποδηλώνει ένα τυπογραφικό λάθος, όπως το να χρησιμοποιήσεις "Hello".lowercase() όταν η σωστή σύνταξη είναι "Hello".lower().
Το "Hello".lowercase() πετάει AttributeError: 'str' object has no attribute 'lowercase'.
Τρέχοντας αυτόν τον κώδικα:
class Robot:
def __init__():
#note that there is no self.direction listed here
self.position = (0, 0)
self.orientation = 'SW'
def forward():
pass
robby = Robot
robby.direction
θα έχει ως αποτέλεσμα ένα stack trace παρόμοιο με αυτό (πρόσεξε το μήνυμα στην τελευταία γραμμή):
robot_simulator_test.py:3: in <module>
from robot_simulator import (
robot_simulator.py:12: in <module>
robby.direction
E AttributeError: type object 'Robot' has no attribute 'direction'
Τρέχοντας αυτόν τον κώδικα:
def distance(strand_a, strand_b):
if strand_a.lowercase() == strand_b:
return 0
distance("ab", "abc")
θα έχει ως αποτέλεσμα ένα stack trace παρόμοιο με αυτό (πρόσεξε το μήνυμα στην τελευταία γραμμή):
def distance(strand_a, strand_b):
> if strand_a.lowercase() == strand_b:
E AttributeError: 'str' object has no attribute 'lowercase'
Ένα ImportError πετιέται όταν ο κώδικας προσπαθεί να εισάγει κάτι, αλλά η Python δεν μπορεί να το κάνει.
Για παράδειγμα, ένα unit test για το Guidos Gorgeous Lasagna κάνει from lasagna import bake_time_remaining, αλλά το αρχείο λύσης lasgana.py μπορεί να μην ορίζει το bake_time_remaining.
Αν τρέξεις το αρχείο lasgana.py χωρίς να έχει οριστεί η συνάρτηση, θα πάρεις το παρακάτω σφάλμα:
We received the following error when we ran your code:
ImportError while importing test module '.mnt.exercism-iteration.lasagna_test.py'.
Hint: make sure your test modules.packages have valid Python names.
Traceback:
.mnt.exercism-iteration.lasagna_test.py:6: in <module>
from lasagna import (EXPECTED_BAKE_TIME,
E ImportError: cannot import name 'bake_time_remaining' from 'lasagna' (.mnt.exercism-iteration.lasagna.py)
During handling of the above exception, another exception occurred:
.usr.local.lib.python3.10.importlib.__init__.py:126: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
.mnt.exercism-iteration.lasagna_test.py:23: in <module>
raise ImportError("In your 'lasagna.py' file, we can not find or import the"
E ImportError: In your 'lasagna.py' file, we can not find or import the function named 'bake_time_remaining()'. Did you mis-name or forget to define it?
### **IndexError**
Python raises an `IndexError` when an invalid index is used to look up a value in a sequence.
This often indicates the index is not computed properly and is often an off-by-one error.
<details>
<summary>Click here for code example</summary>
Consider the following code.
```python
def distance(strand_a, strand_b):
same = 0
for i in range(len(strand_a)):
if strand_a[i] == strand_b[i]:
same += 1
return same
distance("abc", "ab") # Note the first strand is longer than the second strand.
Αν τρέξεις αυτόν τον κώδικα, θα πάρεις ένα σφάλμα παρόμοιο με αυτό. (Πρόσεξε την τελευταία γραμμή.)
hamming_test.py:3: in <module>
from hamming import (
hamming.py:9: in <module>
distance("abc", "ab") # Note the first strand is longer than the second strand.
hamming.py:4: in distance
if strand_a[i] == strand_b[i]:
E IndexError: string index out of range
Όπως και με το IndexError, αυτή η εξαίρεση πετιέται όταν χρησιμοποιείς ένα κλειδί για να ανακτήσεις μια τιμή από ένα λεξικό, αλλά το κλειδί δεν υπάρχει στο λεξικό.
Δες τον παρακάτω κώδικα.
def to_rna(dna_letter):
translation = {"G": "C", "C": "G", "A": "U", "T": "A"}
return translation[dna_letter]
print(to_rna("Q")) # Note, "Q" is not in the translation.
Αν τρέξεις αυτόν τον κώδικα, θα πάρεις ένα σφάλμα παρόμοιο με αυτό. (Πρόσεξε την τελευταία γραμμή.)
rna_transcription_test.py:3: in <module>
from rna_transcription import to_rna
rna_transcription.py:6: in <module>
print(to_rna("Q"))
rna_transcription.py:3: in to_rna
return translation[dna_letter]
E KeyError: 'Q'
Συνήθως, ένα TypeError πετιέται όταν ο λάθος τύπος δεδομένων περνιέται σε μια συνάρτηση ή χρησιμοποιείται σε μια πράξη.
Δες τον παρακάτω κώδικα.
def hello(name): # This function expects a string.
return 'Hello, ' + name + '!'
print(hello(100)) # 100 is not a string.
Αν τρέξεις αυτόν τον κώδικα, θα πάρεις ένα σφάλμα παρόμοιο με αυτό. (Πρόσεξε την τελευταία γραμμή.)
hello_world_test.py:3: in <module>
import hello_world
hello_world.py:5: in <module>
print(hello(100))
hello_world.py:2: in hello
return 'Hello, ' + name + '!'
E TypeError: can only concatenate str (not "int") to str
Ένα ValueError πετιέται συνήθως όταν μια μη έγκυρη τιμή περνιέται σε μια συνάρτηση.
Πρόσεξε, πραγματικές τετραγωνικές ρίζες υπάρχουν μόνο για θετικούς αριθμούς.
Η κλήση math.sqrt(-1) θα πετάξει ValueError: math domain error, αφού το -1 δεν είναι έγκυρη τιμή για μια τετραγωνική ρίζα.
Με (μαθηματικούς) τεχνικούς όρους, το -1 δεν ανήκει στο πεδίο ορισμού των τετραγωνικών ριζών.
import math
math.sqrt(-1)
Αν τρέξεις αυτόν τον κώδικα, θα πάρεις ένα σφάλμα παρόμοιο με αυτό. (Πρόσεξε την τελευταία γραμμή.)
square_root_test.py:3: in <module>
from square_root import (
square_root.py:3: in <module>
math.sqrt(-1)
E ValueError: math domain error
print
Μερικές φορές δεν πετιέται κάποιο σφάλμα, αλλά μια τιμή δεν είναι αυτή που περιμένεις. Αυτό μπορεί να είναι ιδιαίτερα μπερδεμένο αν η τιμή είναι το αποτέλεσμα μιας αλυσίδας υπολογισμών. Σε τέτοια περίπτωση μπορεί να βοηθήσει να κοιτάξεις την τιμή σε κάθε βήμα, για να δεις σε ποιο βήμα δεν συμπεριφέρεται όπως περιμένεις. Η συνάρτηση print μπορεί να χρησιμοποιηθεί για την εκτύπωση της τιμής στην κονσόλα. Το παρακάτω είναι ένα παράδειγμα μιας συνάρτησης που δεν επιστρέφει την αναμενόμενη τιμή:
# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
return (num / 2) * 4
Όταν στη συνάρτηση περαστεί το 5, η αναμενόμενη τιμή είναι 8, αλλά επιστρέφει 10.0.
Για να εντοπίσεις το πρόβλημα, ο υπολογισμός σπάει σε βήματα, ώστε να μπορείς να ελέγξεις την τιμή σε κάθε βήμα.
# the intent is to pass an integer to this function and get an integer back
def halve_and_quadruple(num):
# verify the number in is what is expected
# prints 5
print(num)
# we want the int divided by an integer to be an integer
# but this prints 2.5! We've found our mistake.
print(num / 2)
# this makes sense, since 2.5 x 4 = 10.0
print((num / 2) * 4)
return (num / 2) * 4
What the `print` calls revealed is that we used `/` when we should have used `//`, the [floor division operator][floor division operator].
## Logging
[Logging][logging] can be used similarly to `print`, but it is more powerful.
What is logged can be configured by the logging severity (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'.)
A call to the `logging.error` function can pass `True` to the `exc_info` parameter, which will additionally log the stack trace.
By configuring multiple handlers, logging can write to more than one place with the same logging function.
Following is an example of logging printed to the console:
```python
>>> import logging
>>>
>>> # configures minimum logging level as INFO
>>> logging.basicConfig(level=logging.INFO)
>>>
>>> def halve_and_quadruple(num):
... # prints INFO:root: num == 5
... logging.info(f" num == {num}")
... return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))
Το επίπεδο έχει ρυθμιστεί σε INFO επειδή το προεπιλεγμένο επίπεδο είναι WARNING.
Για μια μόνιμη καταγραφή, ο logger μπορεί να ρυθμιστεί ώστε να γράφει σε ένα αρχείο, ως εξής:
>>> import logging
...
>>> # configures the output file name to example.log, and the minimum logging level as INFO
>>> logging.basicConfig(filename='example.log', level=logging.INFO)
...
... def halve_and_quadruple(num):
... # prints INFO:root: num == 5 to the example.log file
... logging.info(f" num == {num}")
... return (num // 2) * 4
...
>>> print(halve_and_quadruple(5))
assert είναι μια εντολή που θα πρέπει πάντα να αξιολογείται ως True, εκτός αν υπάρχει κάποιο bug στο πρόγραμμα.
Όταν ένα assert αξιολογείται ως False, θα πετάξει ένα AssertionError.
Το traceback για το AssertionError μπορεί να περιλαμβάνει ένα προαιρετικό μήνυμα που αποτελεί μέρος της εντολής assert.
Αν και το μήνυμα είναι προαιρετικό, είναι καλή πρακτική να περιλαμβάνεις πάντα ένα στον ορισμό του assert.
Το παρακάτω είναι ένα παράδειγμα χρήσης του assert:
>>> def int_division(dividend, divisor):
... assert divisor != 0, "divisor must not be 0"
... return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, 0))
Traceback (most recent call last):
File <stdin>, line 7, in <module>
print(int_division(2, 0))
^^^^^^^^^^^^^^^^^^
File <stdin>, line 2, in int_division
assert divisor != 0, "divisor must not be 0"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must not be 0
Αν αρχίσεις να διαβάζεις το traceback από το κάτω μέρος (όπως πρέπει), βλέπεις γρήγορα ότι το πρόβλημα είναι ότι το 0 δεν θα έπρεπε να περνιέται ως divisor.
Το assert μπορεί επίσης να χρησιμοποιηθεί για να ελέγξεις ότι μια τιμή είναι του αναμενόμενου τύπου:
>>> import numbers
...
...
... def int_division(dividend, divisor):
... assert divisor != 0, "divisor must not be 0"
... assert isinstance(divisor, numbers.Number), "divisor must be a number"
... return dividend // divisor
...
>>> print(int_division(2, 1))
2
>>> print(int_division(2, '0'))
Traceback (most recent call last):
File <stdin>, line 11, in <module>
print(int_division(2, '0'))
^^^^^^^^^^^^^^^^^^^^
File <stdin>, line 6, in int_division
assert isinstance(divisor, numbers.Number), "divisor must be a number"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: divisor must be a number
Μόλις εντοπιστεί ένα bug, σκέψου να αντικαταστήσεις το assert με χειρισμό σφαλμάτων.
Αυτό συμβαίνει επειδή όλες οι εντολές assert μπορούν να απενεργοποιηθούν τρέχοντας την Python με τις επιλογές -O ή -OO, ή ορίζοντας τη μεταβλητή περιβάλλοντος PYTHONOPTIMIZE σε 1 ή 2.
Ο ορισμός του PYTHONOPTIMIZE σε 1 ισοδυναμεί με την εκτέλεση της Python με την επιλογή -O, που απενεργοποιεί τις εντολές assert.
Ο ορισμός του PYTHONOPTIMIZE σε 2 ισοδυναμεί με την εκτέλεση της Python με την επιλογή -OO, που και απενεργοποιεί τις εντολές assert και αφαιρεί τα docstrings από τον bytecode.
Η μείωση του bytecode είναι ένας τρόπος να τρέξει ο κώδικας πιο γρήγορα.
Η Python έχει έναν ενσωματωμένο debugger, το pdb.
Μπορείς να τον χρησιμοποιήσεις για να εκτελέσεις τον κώδικα βήμα βήμα και να εξετάσεις τις μεταβλητές.
Μπορείς επίσης να ορίσεις breakpoints με αυτόν.
Για να ξεκινήσεις, πρέπει πρώτα να κάνεις import pdb και μετά να καλέσεις το pdb.set_trace() εκεί όπου θέλεις να ξεκινήσεις το debugging:
import pdb
def add(num1, num2):
return num1 + num2
pdb.set_trace()
sum = add(1,5)
print(sum)
Τρέχοντας αυτόν τον κώδικα θα σου εμφανιστεί ένα prompt του pdb, όπου μπορείς να πληκτρολογήσεις εντολές.
Γράψε help για να πάρεις μια λίστα με τις εντολές.
Οι πιο συνηθισμένες είναι η step, που μπαίνει μέσα σε μια συνάρτηση που καλείται σε εκείνη τη γραμμή.
Η next προσπερνά μια κλήση συνάρτησης και πηγαίνει στην επόμενη γραμμή. Η where σου λέει σε ποια γραμμή βρίσκεσαι.
Μερικές άλλες χρήσιμες εντολές είναι η whatis <variable>, που σου λέει τον τύπο μιας μεταβλητής, και η print(<variable>), που τυπώνει την τιμή μιας μεταβλητής.
Μπορείς επίσης να χρησιμοποιήσεις απλώς το <variable> για να τυπώσεις την τιμή μιας μεταβλητής.
Μια άλλη εντολή είναι η jump <line number>, που πηγαίνει σε έναν συγκεκριμένο αριθμό γραμμής.
Ακολουθεί ένα μικρό παράδειγμα χρήσης του debugger, με βάση τον προηγούμενο κώδικα. Σημείωσε ότι σε αυτό και στα επόμενα παραδείγματα, οι πλατφόρμες MacOS ή Linux θα έχουν διαδρομές αρχείων με καθέτους:
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> step
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)
>>> whatis num1
... <class 'int'>
>>> print(num2)
... 5
>>> next
... > c:\pdb.py(4)add()
... -> return num1 + num2
... (Pdb)
>>> jump 3
... > c:\pdb.py(3)add()
... -> def add(num1, num2):
... (Pdb)
Τα breakpoints ορίζονται με το break <filename>:<line number> <condition>, όπου η συνθήκη είναι προαιρετική και πρέπει να είναι αληθής για να ενεργοποιηθεί το breakpoint.
Μπορείς απλώς να γράψεις break για να πάρεις μια λίστα με τα breakpoints που έχεις ορίσει.
Για να απενεργοποιήσεις ένα breakpoint, μπορείς να γράψεις disable <breakpoint number>.
Για να ενεργοποιήσεις ένα breakpoint, μπορείς να γράψεις enable <breakpoint number>.
Για να διαγράψεις ένα breakpoint, μπορείς να γράψεις clear <breakpoint number>.
Για να συνεχίσεις την εκτέλεση, μπορείς να γράψεις continue ή c. Για να βγεις από τον debugger, μπορείς να γράψεις quit ή q.
Ακολουθεί ένα παράδειγμα χρήσης των παραπάνω εντολών του debugger, με βάση τον προηγούμενο κώδικα:
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> break
...
>>> break pdb:4
... Breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type Disp Enb Where
... 2 breakpoint keep yes at c:\pdb.py:4
>>> c # continue
... > c:\pdn.py(4)add()
... -> return num1 + num2
>>> disable break 1
... Disabled breakpoint 1 at c:\pdb.py:4
>>> break
... Num Type Disp Enb Where
... 1 breakpoint keep no at c:\pdb.py:4
... breakpoint already hit 1 time
>>> clear break 1
... Deleted breakpoint 1 at c:\pdb.py:4
>>> break
...
Στην Python 3.7+ υπάρχει ένας πιο εύκολος τρόπος να δημιουργήσεις breakpoints.
Το να γράψεις απλώς breakpoint() εκεί όπου το χρειάζεσαι θα δημιουργήσει ένα.
def add(num1, num2):
breakpoint()
return num1 + num2
breakpoint()
sum = add(1,5)
print(sum)
>>> python pdb.py
... > c:\pdb.py(7)<module>()
... -> sum = add(1,5)
... (Pdb)
>>> c # continue
... > c:\pdb.py(5)add()
... -> return num1 + num2