Улучшенная диагностика исключений в Python 3.11

Share
Share
Send

В больших программах сложно понять по распечатке стека вызовов где именно произошла ошибка в коде. Тем более если используются сложные выражения. В 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.

Надежный Python

Присоединись к культуре надежного программирования на Python! Новости, события, мнения, обновления библиотек Python на одном сайте.