Покращена діагностика винятків у 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 на одному сайті.