Огляд того, як читати трасування Python для налагодження.
Коли ми викликаємо функцію, створюється обʼєкт кадру, який зберігає локальні змінні та аргументи, передані у функцію.
Коли функція повертає результат, обʼєкт кадру знищується.
Коли функція B викликається всередині функції A, значення функції B потрапляють в обʼєкт кадру, який потім розміщується поверх обʼєкта кадру функції A у стеку викликів.
Стек викликів - це набір обʼєктів кадрів для функцій, активних на цей момент.
Якщо функція A викликала функцію B, а функція B викликала функцію C, то обʼєкти кадрів усіх трьох функцій перебуватимуть у стеку викликів.
Щойно функція C поверне результат, її обʼєкт кадру буде знято зі стека, і в стеку викликів залишаться лише обʼєкти кадрів функцій A і B.
Трасування стека - це звіт про всі обʼєкти кадрів у стеку на певний момент часу. Коли програма на Python натрапляє на необроблений виняток, вона виводить повідомлення про виняток і трасування стека. Трасування стека показує, де виник виняток і які функції викликалися до цього.
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)
У трасуваннях найновіший виклик стоїть останнім, тому читати трасування варто починати з винятку внизу. Рухаючись звідти вгору, ми бачимо, як дійшли до цієї інструкції. Якщо помістити проблемний рядок у функцію і потім викликати цю функцію, побачимо довше трасування:
>>> 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
дасть трасування стека, схоже на це (зверніть увагу на повідомлення в останньому рядку):
.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")
дасть трасування стека, схоже на це (зверніть увагу на повідомлення в останньому рядку):
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 виникає, коли код (або модульний тест!) намагається звернутися до атрибута обʼєкта, але в цього обʼєкта такого атрибута немає.
Наприклад, модульний тест розраховує, що обʼєкт 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
дасть трасування стека, схоже на це (зверніть увагу на повідомлення в останньому рядку):
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")
дасть трасування стека, схоже на це (зверніть увагу на повідомлення в останньому рядку):
def distance(strand_a, strand_b):
> if strand_a.lowercase() == strand_b:
E AttributeError: 'str' object has no attribute 'lowercase'
ImportError виникає, коли код намагається щось імпортувати, але Python не може цього зробити.
Наприклад, модульний тест для 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))
The level is configured as INFO because the default level is WARNING.
For a persistent log, the logger can be configured to write to a file, like so:
>>> 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 is a statement which should always evaluate to True unless there is a bug in the program.
When an assert evaluates to False it will raise an AssertionError.
The Traceback for the AssertionError can include an optional message that is part of the assert statement.
Although a message is optional, it is good practice to always include one in the assert definition.
The following is an example of using 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
If we start reading the Traceback at the bottom (as we should) we quickly see the problem is that 0 should not be passed as the divisor.
assert can also be used to test that a value is of the expected type:
>>> 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
Once a bug is identified, consider replacing the assert with error handling.
This is because all assert statements can be disabled through running Python with the -O or -OO options, or from setting the PYTHONOPTIMIZE environment variable to 1 or 2.
Setting PYTHONOPTIMIZE to 1 is equivalent to running Python with the -O option, which disables assertions.
Setting PYTHONOPTIMIZE to 2 is equivalent to running Python with the -OO option, which both disables assertions and removes docstrings from the bytcode.
Reducing bytecode is one way to make the code run faster.
Python has a built in debugger, pdb.
It can be used to step through code and inspect variables.
You can also set breakpoints with it.
To get started you have to first import pdb and then call pdb.set_trace() where you want to start debugging:
import pdb
def add(num1, num2):
return num1 + num2
pdb.set_trace()
sum = add(1,5)
print(sum)
Running this code will give you a pdb prompt where you can type in commands.
Write help to get a list of commands.
The most common ones are step which steps into a function called at that line.
next steps over a function call and move to the next line. where tells you which line you are on.
Some other useful commands are whatis <variable> which tells you the type of a variable and print(<variable>) which prints the value of a variable.
You can also just use <variable> to print the value of a variable.
Another command is jump <line number> which jumps to a specific line number.
Here is a small example of how to use the debugger based on the code earlier. Note that for this and following examples, MacOS or Linux platforms would have file paths using forward slashes:
>>> 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 are set up by break <filename>:<line number> <condition> where the condition is an optional condition that has to be true for the breakpoint to be hit.
You can simply write break to get a list of the breakpoints you have set.
To disable a breakpoint you can write disable <breakpoint number>.
To enable a breakpoint you can write enable <breakpoint number>.
To delete a breakpoint you can write clear <breakpoint number>.
To continue execution you can write continue or c. To exit the debugger you can write quit or q.
Here is an example of how to use the above debugger commands based on the code earlier:
>>> 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
...
In Python 3.7+ there is an easier way to create breakpoints.
Simply writing breakpoint() where needed will create one.
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