Улучшенная диагностика исключений в Python 3.11
В больших программах сложно понять по распечатке стека вызовов где именно произошла ошибка в коде. Тем более если используются сложные выражения. В Python 3.11 ввели подробную диагностическую информацию в распечатку стека вызовов. Рассмотрим несколько примеров.
На момент написания статьи Python 3.11 еще официально не вышел. Можно установить версию 3.11 отдельно со страницы пререлизов.
Проверить версию Python можно опцией --version
в консоли:
% python --version
Python 3.11.0b3
Рассмотрим небольшой пример (файл main.py
) в котором потенциально может возникнуть исключение ZeroDivisionError
в функции f1
:
# main.py
import sys
def f1(a: float, b: float, c: float) -> float:
return a / b / c
a1 = float(sys.argv[1])
b1 = float(sys.argv[2])
c1 = float(sys.argv[3])
print(f"Result: {f1(a1, b1, c1) * sum([a1, b1, c1])}")
Запустим этот пример из командной строки. Здесь мы умышленно передаем ноль в качестве одного из аргументов, чтобы получить исключение ZeroDivisionError
:
python main.py 1 0 2
Как выглядит распечтка стека вызовов в Python 3.10:
Traceback (most recent call last):
File "main.py", line 12, in <module>
print(f"Result: {f1(a1, b1, c1) * sum([a1, b1, c1])}")
File "main.py", line 6, in f1
return a / b / c
ZeroDivisionError: float division by zero
Обратите внимание: интерпретатор сообщает об исключении ZeroDivisionError
в 6 строчке, но не указывает в каком именно операторе деления: первом или втором?
Запустим тот же самый код с теми же самыми аргументами в интерпретаторе Python 3.11. Как выглядит распечатка стека вызовов в Python 3.11:
Traceback (most recent call last):
File "main.py", line 12, in <module>
print(f"Result: {f1(a1, b1, c1) * sum([a1, b1, c1])}")
^^^^^^^^^^^^^^
File "main.py", line 6, in f1
return a / b / c
~~^~~
ZeroDivisionError: float division by zero
На что в этой распечтаке следует обратить внимание:
- символом
~~^~~
подчеркнуто, что именно в первом операторе происходит деление на ноль:a / b
; - символом
^^^^^^^^^^^^^^
подчеркнуто та часть сложного выражения, при вычислении которой произошло исключение:f1(a1, b1, c1)
.
Попробуем запуситить пример в интерпретаоре Python 3.11 с другими аргументами:
python main.py 1 2 0
Теперь интерпретатор укажет на второй оператор деления:
File "main.py", line 6, in f1
return a / b / c
~~~~~~^~~
ZeroDivisionError: float division by zero
Работа со сложными словарями — это еще один пример, где улучшенная диагностика поможет быстрее найти ошибку. Рассмотрим JSON-документ data.json
и код файла account.py
, который суммирует балансы счетов account1
и account2
. Обратите внимание, что в этом примере умышленно нет транзакций в account2
.
{
"account1": {
"transactions": [
{"id": "1", "title": "Покупка", "sum": 123, "balance": 1000},
{"id": "2", "title": "Покупка", "sum": 800, "balance": 200},
{"id": "3", "title": "Покупка", "sum": 15, "balance": null}
]
},
"account2": {
"transactions": null
}
}
# account.py
import json
with open('data.json') as f:
data = json.load(f)
summa = data['account1']['transactions'][0]['balance'] + data['account2']['transactions'][0]['balance']
print(f"Сумма счетов: {summa}")
Если мы запустим этот скрипт интерпретатором Python 3.10, то получим такую распечатку стека вызвов:
Traceback (most recent call last):
File "account.py", line 7, in <module>
summa = data['account1']['transactions'][0]['balance'] + data['account2']['transactions'][0]['balance']
TypeError: 'NoneType' object is not subscriptable
Здесь понятно, что исключение происходит в 7 строке, но непонятно, о каком именно объекте идет речь? Ведь выражение сложное. Требуется перезапускать программу под отладчиком, ставить точку останова и смотреть состояние внутренних переменных.
Если мы запустим этот скрипт интерпретатором Python 3.11, то получим такую распечатку стека вызвов:
File "account.py", line 7, in <module>
summa = data['account1']['transactions'][0]['balance'] + data['account2']['transactions'][0]['balance']
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^
TypeError: 'NoneType' object is not subscriptable
Здесь с помощью символа ^^^
показано какой именно оператор вызвал исключение: результат выражения data['account2']['transactions']
равен None
и к нему нельзя применить [0]
.
Полная информация о возможностях подробной диагностики в исключениях можно прочитать в PEP 657 – Include Fine Grained Error Locations in Tracebacks.