Для чего нужны новые инструменты?

Видели когда-нибудь такой вывод ошибок?

2 divided by 1 is equal to 2.0.
Traceback (most recent call last):
  File "loguru_example.py", line 17, in <module>
    divide_numbers(num_list)
  File "loguru_example.py", line 11, in divide_numbers
    res = division(num1, num2)
  File "loguru_example.py", line 5, in division
    return num1/num2
ZeroDivisionError: division by zero

А хотели бы, чтобы вывод был немного более понятным и наглядным? Как показано здесь:

Изображение автора

Или даже сделать визуализацию того, какие строки кода выполняются и сколько раз они выполняются в режиме реального времени:

Гифка автора

Именно это помогут сделать инструменты, о которых пойдет речь дальше в статье. Вот эти три инструмента.

  • Loguru  —  для лучшего вывода исключений.
  • Snoop  —  печатает строки кода, выполняемого в функции.
  • Heartrate  —  визуализирует выполнение программы на Python в режиме реального времени.

И для использования этих инструментов нужна всего одна строка кода!

Loguru  —  для лучшего вывода исключений

Loguru  —  это библиотека, делающая логирование на Python приятным. Loguru предоставляет много интересных функциональных возможностей, самая полезная из которых  —  возможность отлавливать неожиданные ошибки и отображать значение переменной, которое приводит к сбою кода.

Устанавливается Loguru так:

pip install loguru

Разберемся, чем так хорош Loguru. Представьте, что у вас две функции division и divide_numbers и выполняется функция divide_numbers:

from itertools import combinations

def division(num1: int, num2: int):
    return num1/num2

def divide_numbers(num_list: list):
  """Division of 2 numbers in the number list """
  
    for comb in combinations(num_list, 2):
        num1, num2 = comb 
        res = division(num1, num2)
        print(f"{num1} divided by {num2} is equal to {res}.")


if __name__ =='__main__':
    num_list = [2, 1, 0]
    divide_numbers(num_list)

Обратите внимание: combinations([2,1,0], 2) возвращает [(2, 1), (2, 0), (1, 0)]. После выполнения этого кода получаем такую ошибку:

2 divided by 1 is equal to 2.0.
Traceback (most recent call last):
  File "loguru_example.py", line 17, in <module>
    divide_numbers(num_list)
  File "loguru_example.py", line 11, in divide_numbers
    res = division(num1, num2)
  File "loguru_example.py", line 5, in division
    return num1/num2
ZeroDivisionError: division by zero

По этому выводу мы видим, что ошибка возникает в строке return num1/num2, но не знаем, которое из значений (num1 или num2) вызывает ее. К счастью, это легко отследить, добавив декоратор Loguru logger.catch:

from loguru import logger 
from itertools import combinations

def division(num1: int, num2: int):
    return num1/num2

@logger.catch # Добавляем это для отслеживания ошибок
def divide_numbers(num_list: list):
    for comb in combinations(num_list, 2):
        num1, num2 = comb 
        res = division(num1, num2)
        print(f"{num1} divided by {num2} is equal to {res}.")


if __name__ =='__main__':
    num_list = [2, 1, 0]
    divide_numbers(num_list)

Вывод:

Изображение автора

Добавили logger.catch, и теперь исключения гораздо проще трактовать! Оказывается, ошибка возникает при делении 2 на 0.

Snoop  —  печатает строки кода, выполняемого в функции

А что, если в коде нет ошибки, но мы хотим выяснить, что в нем происходит? Здесь пригодится snoop.

Snoop  —  это пакет Python, который печатает строки выполняемого кода вместе со значениями каждой переменной. Для этого надо добавить всего лишь один декоратор.

Устанавливается snoop так:

pip install snoop

Допустим, у нас есть функция factorial, которая находит факториал целого числа:

import snoop 

def factorial(x: int):
    if x == 1:
        return 1
    else:
        return (x * factorial(x-1))
        
if __name__ == "__main__":
    num = 5
    print(f"The factorial of {num} is {factorial(num)}")

Вывод:

The factorial of 5 is 120

Разобраться, почему факториал factorial(5) равен 20, помогает добавление к функции factorial декоратора snoop:

import snoop 

@snoop
def factorial(x):
    if x == 1:
        return 1
    else:
        return (x * factorial(x-1))


if __name__ == "__main__":
    num = 5
    print(f"The factorial of {num} is {factorial(num)}")

Вывод:

Изображение автора

В этом выводе показываются значения переменных и то, какие строки кода выполняются. Теперь рекурсия понятна намного лучше!

Heartrate  —  визуализирует выполнение программы на Python в режиме реального времени

Для визуализации того, какие строки кода выполняются и сколько раз, попробуйте heartrate.

heartrate  —  инструмент от создателя snoop. Устанавливается heartrate так:

pip install heartrate

Теперь добавим в предыдущий код heartrate.trace(browser=True). Откроется окно браузера, отображающее визуализацию файла, в котором вызван этот trace():

import heartrate 
heartrate.trace(browser=True)

def factorial(x):
    if x == 1:
        return 1
    else:
        return (x * factorial(x-1))


if __name__ == "__main__":
    num = 5
    print(f"The factorial of {num} is {factorial(num)}")

При запуске этого кода должен появиться новый браузер. Если не появился, перейдите по адресу http://localhost:9999. Вот такой вывод вы должны увидеть:

Изображение автора

Темно- и светло-синие полосы показывают строки кода, которые выполнялись. Чем длиннее полосы, тем больше было выполнений, более светлые цвета соответствуют самым последним выполнениям.

По этому выводу мы видим, что программа выполняется:

  • if x==1 пять раз.
  • return 1 один раз.
  • return (x * factorial(x-1)) четыре раза.

Вывод имеет смысл, так как исходное значение x равно 5 и функция вызывается повторно до тех пор, пока x не станет равным 1.

Теперь посмотрим, как визуализируется выполнение программы на Python в режиме реального времени с помощью heartrate. Добавим sleep(0.5), чтобы программа работала немного медленнее, а num увеличилось до 20:

import heartrate 
from time import sleep

heartrate.trace(browser=True)


def factorial(x):
    if x == 1:
        return 1
    else:
        sleep(0.5)
        return (x * factorial(x-1))


if __name__ == "__main__":
    num = 20
    print(f"The factorial of {num} is {factorial(num)}")
Гифка автора

Теперь мы видим, какие строки кода выполняются и сколько раз каждая из них выполнялась в режиме реального времени.

Заключение

Поздравляю! Вы только что освоили три инструмента для отслеживания и визуализации выполнения кода на Python. Надеюсь, что с ними отладка будет для вас более легкой и беспроблемной. И раз уж этим инструментам нужна всего одна строка кода, почему бы не испытать их в деле?

Смело дублируйте исходный код этой статьи и экспериментируйте здесь.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Khuyen Tran: 3 Tools to Track and Visualize the Execution of your Python Code

Предыдущая статьяОднопоточность и асинхронность: как у Node это получается?
Следующая статьяБудет ли ИИ главенствовать в 2021 году? Большой вопрос