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

Обоснование актуальности

При изучении алгоритмов лучше всего ознакомиться не только с их описанием и возможными реализациями в общем виде, но и то, как они работают на конкретных данных. Частично эту потребность покрывает отладчик (debugger), но значительно удобнее смотреть на gif картинку, на которой отображается только полезная информация, запрашиваемая пользователем. Визуализация алгоритмов часто оказывается более полезной, чем традиционный отладчик в определенных контекстах, благодаря своей способности обеспечивать высокоуровневое интуитивное представление всего алгоритмического процесса. Хотя отладчики превосходно справляются с выявлением и решением конкретных проблем в коде, им может не удаться понять общий ход и логику сложных алгоритмов. Визуализация алгоритма предлагает целостное представление, позволяя разработчикам наблюдать последовательное выполнение каждого шага и наблюдать за преобразованиями данных в графическом формате. Такая визуальная ясность особенно полезна для понимания сложных алгоритмов с многочисленными точками принятия решений и сложными ветвлениями. В отличие от отладчиков, которые сосредоточены на изоляции ошибок, визуализация алгоритма позволяет разработчикам понять поведение алгоритма в целом, помогая как выявлять неэффективности, так и оптимизировать общую структуру алгоритма. Эта более широкая перспектива может сыграть важную роль в создании более надежных и эффективных алгоритмов в различных вычислительных приложениях.

Поставленные цели и задачи

Цель моего мини-проекта заключается в создании прототипа библиотеки, с помощью которой было бы удобно создавать анимации алгоритмов. Для ее реализации решаются задачи:

  1. Разобраться с ООП в python, освоить работу с классами и “магическими методами”;

  2. Разобраться с библиотекой Pillow, понять, как создавать отдельные изображения и gif анимации;

  3. Анимировать перестановку и присвоение элементов одномерного массива;

  4. Проверить работу библиотеки на простых алгоритмах.

Дорожная карта

Чтобы наиболее точно разобраться в материале и не потеряться, я составил дорожную карту. Ее можно разделить на 3 части:

  1. Магические методы (init, setitem, getitem)

Магические методы — это специальные методы Python, которые начинаются и заканчиваются двойным подчеркиванием. Эти методы позволяют определить, как объекты ведут себя в различных обстоятельствах. Метод init — это важнейший магический метод Python, служащий конструктором класса. Он автоматически вызывается при создании объекта из класса и используется для инициализации его атрибутов.
Методы setitem и getitem — это магические методы, связанные с индексацией и подпиской в Python. Они используются, когда экземпляры класса рассматриваются как контейнеры или сопоставления. Метод setitem вызывается, когда элементу присваивается определенный индекс, а метод getitem вызывается, когда доступ к элементу осуществляется с использованием индекса.

  1. Библиотека Pillow

Прототип библиотеки разрабатывается с применением библиотеки Pillow [4]. В данном случае использовалась часть функций и методов для решения трех конкретных задач:

  1. Создание нового изображения;

  2. Рисование поверх изображения (в том числе – текста);

  3. Сохранения массива изображений в виде .gif анимации. Ниже представлен пример скрипта, который создает новое изображение:

self.data[index] = value

width = (cellSize + space + 2) * len(self.data)
height = cellSize * 6

dx = cellSize
dy = cellSize / 2

img = Image.new('RGB', (width, height), (255, 255, 255))
draw = ImageDraw.Draw(img)

font = ImageFont.truetype("arial.ttf", 15)

for i, n in enumerate(self.data):
    draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                    50 - cellSize / 2 + dy,
                    i * cellSize + cellSize / 2 + i * space + dx,
                    50 + cellSize / 2 + dy],
                    fill = None, outline = (0, 0, 0))

    if n != None:
        draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                    50 - cellSize / 2 + dy],
                    text = str(n), fill = (0, 0, 0), font = font)
  1. Анимация: текстовое описание, вывод формул

На рисунке ниже представлена схема, согласно которой формируется изображение массива. space - это расстояние между квадратами справа и слева, cellSize - сторона квадрата. Высота gif картинки равна cellSize*5, а длина уже зависит от количества квадратов, то есть от количества чисел в массиве.

Схема изображения массива
Схема изображения массива

Этот макет позволяет составить следующие формулы:

draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                50 - cellSize / 2 + dy,
                i * cellSize + cellSize / 2 + i * space + dx,
                50 + cellSize / 2 + dy],
                fill = None, outline = (0, 0, 0))
draw.text([i * cellSize - cellSize / 2 + i * space + dx,
            50 - cellSize / 2 + dy],
            text = str(n), fill = (0, 0, 0), font = font)

На рисунке ниже представлен макет анимации, согласно которому происходит движение красного и зеленого квадратов. Так как число в красном квадрате меньше чем в зеленом, то происходит их замена. Красный квадрат сдвигается вверх, а зеленый вниз, и уже в дальнейшем они меняются местами.

Схема анимации
Схема анимации

На языке Python это можно описать следующим образом:

left_x = left * cellSize + space * left
right_x = right * cellSize + space * right

left_y = 50
right_y = 50

main_frame = 0

width = (cellSize + space + 2) * len(self.data)
height = cellSize * 6

dx = cellSize
dy = cellSize / 2

font = ImageFont.truetype("arial.ttf", 15)

for main_frame in range(30):
    img = Image.new('RGB', (width, height), (255, 255, 255))
    draw = ImageDraw.Draw(img)

    for i, n in enumerate(self.data):
        if i not in [left, right]:
            draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                            50 - cellSize / 2 + dy,
                            i * cellSize + cellSize / 2 + i * space + dx,
                            50 + cellSize / 2 + dy],
                            fill = None, outline = (0, 0, 0))

            draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                        50 - cellSize / 2 + dy],
                        text = str(n), fill = (0, 0, 0), font = font)

    draw.rectangle([left_x - cellSize / 2 + dx,
                    left_y - cellSize / 2 + dy,
                    left_x + cellSize / 2 + dx,
                    left_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

    draw.text([left_x - cellSize / 2 + dx,
                left_y - cellSize / 2 + dy],
                text = str(self.data[left]),
                fill = (0, 0, 0),
                font = font)

    draw.rectangle([right_x - cellSize / 2 + dx,
                    right_y - cellSize / 2 + dy,
                    right_x + cellSize / 2 + dx,
                    right_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

    draw.text([right_x - cellSize / 2 + dx,
                right_y - cellSize / 2 + dy],
                text = str(self.data[right]),
                fill = (0, 0, 0),
                font = font)

    main_frame += 1

    if main_frame < 10:
        left_y += 5
        right_y -= 5

    elif main_frame < 20:
        left_x -= (left - right) * (cellSize + space) / 10
        right_x += (left - right) * (cellSize + space) / 10

    elif main_frame < 29:
        left_y -= 5
        right_y += 5

    self.frames.append(img)

self.data[left], self.data[right] = self.data[right], self.data[left]

Мой результат

В рамках разработки была применена сортировка пузырьком:

arr = AnimatedList(size = 10)

for i in range(10):
    arr[i] = randint(-10, 10)

for i in range(10):
    for j in range(9):
        if arr.data[j] > arr.data[j + 1]:
            arr.swap(j, j + 1)
arr.save_animation('animation.gif')

Данный тип сортировки формирует gif анимацию. Gif анимация представляет собой перемещение элементов массива, который схематически показан на рисунке ниже.

Схема анимации
Схема анимации

Так как анимация выполняется без ошибок, то библиотека написана корректно. Следовательно, цель, поставленная в проекте, достигнута! Уррра!

Полный код

from random import randint
from PIL import Image, ImageDraw, ImageFont


class AnimatedList:
    def __init__(self, data = [], size = 0):
        self.frames = []

        self.data = [None for i in range(size)]

        if data:
            self.data = data

    def __setitem__(self, index, value):
        self.data[index] = value

        width = (cellSize + space + 2) * len(self.data)
        height = cellSize * 6

        dx = cellSize
        dy = cellSize / 2

        img = Image.new('RGB', (width, height), (255, 255, 255))
        draw = ImageDraw.Draw(img)

        font = ImageFont.truetype("arial.ttf", 15)

        for i, n in enumerate(self.data):
            draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                            50 - cellSize / 2 + dy,
                            i * cellSize + cellSize / 2 + i * space + dx,
                            50 + cellSize / 2 + dy],
                           fill = None, outline = (0, 0, 0))

            if n != None:
                draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                           50 - cellSize / 2 + dy],
                          text = str(n), fill = (0, 0, 0), font = font)

        self.frames.append(img)

    def __getitem__(self, *args):
        pass

    def swap(self, left, right):
        left_x = left * cellSize + space * left
        right_x = right * cellSize + space * right

        left_y = 50
        right_y = 50

        main_frame = 0

        width = (cellSize + space + 2) * len(self.data)
        height = cellSize * 6

        dx = cellSize
        dy = cellSize / 2

        font = ImageFont.truetype("arial.ttf", 15)
        
        for main_frame in range(30):
            img = Image.new('RGB', (width, height), (255, 255, 255))
            draw = ImageDraw.Draw(img)

            for i, n in enumerate(self.data):
                if i not in [left, right]:
                    draw.rectangle([i * cellSize - cellSize / 2 + i * space + dx,
                                    50 - cellSize / 2 + dy,
                                    i * cellSize + cellSize / 2 + i * space + dx,
                                    50 + cellSize / 2 + dy],
                                   fill = None, outline = (0, 0, 0))

                    draw.text([i * cellSize - cellSize / 2 + i * space + dx,
                               50 - cellSize / 2 + dy],
                              text = str(n), fill = (0, 0, 0), font = font)

            draw.rectangle([left_x - cellSize / 2 + dx,
                            left_y - cellSize / 2 + dy,
                            left_x + cellSize / 2 + dx,
                            left_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

            draw.text([left_x - cellSize / 2 + dx,
                       left_y - cellSize / 2 + dy],
                      text = str(self.data[left]),
                      fill = (0, 0, 0),
                      font = font)

            draw.rectangle([right_x - cellSize / 2 + dx,
                            right_y - cellSize / 2 + dy,
                            right_x + cellSize / 2 + dx,
                            right_y + cellSize / 2 + dy], fill = None, outline = (0, 0, 0))

            draw.text([right_x - cellSize / 2 + dx,
                       right_y - cellSize / 2 + dy],
                      text = str(self.data[right]),
                      fill = (0, 0, 0),
                      font = font)

            main_frame += 1

            if main_frame < 10:
                left_y += 5
                right_y -= 5

            elif main_frame < 20:
                left_x -= (left - right) * (cellSize + space) / 10
                right_x += (left - right) * (cellSize + space) / 10

            elif main_frame < 29:
                left_y -= 5
                right_y += 5

            self.frames.append(img)

        self.data[left], self.data[right] = self.data[right], self.data[left]

    def save_animation(self, name):
        self.frames[0].save('/Users/egorkluychikov/Downloads'+name,
                            save_all = True,
                            append_images = self.frames[1:],
                            duration = 100,
                            loop = 0)

cellSize = 20

space = 5

arr = AnimatedList(size = 10)

for i in range(10):
    arr[i] = randint(-10, 10)

for i in range(10):
    for j in range(9):
        if arr.data[j] > arr.data[j + 1]:
            arr.swap(j, j + 1)
arr.save_animation('animation.gif')

Список источников:

  1. Что такое объектно-ориентированное программирование (ООП)? - URL: https://itanddigital.ru/oop

  2. Какая разница между объектом и классом. - URL: https://uchet-jkh.ru/i/kakaya-raznica-mezdu-obektom-i-klassom - Дата публикации: 19 августа 2023.

  3. Что такое Self в Python — подробно на примерах. - URL: https://python-lab.ru/chto-takoe-self-v-python-podrobno-na-primerah

  4. Pillow (PIL Fork) 10.2.0 documentation. - URL: https://pillow.readthedocs.io/en/stable/index.html

Буду рад услышать от вас вопросы и предложения, потому что сделан только прототип и его еще нужно доводить до ума.

Комментарии (4)



  1. Vitaliy_dzen
    07.10.2024 04:16

    Что же Вы не указываете на то, что используется шрифт, который например у меня на чистом Linux Mint отсутствует

    font = ImageFont.truetype("arial.ttf", 15)

    и дирректории

    self.frames[0].save('/Users/egorkluychikov/Downloads'+name,

    в линуксе как бы не существует...

    Указывайте в следующий раз на такие моменты, а то новички могут и "поплыть"

    Занудствовать не хотел.


  1. Shaha272
    07.10.2024 04:16

    Python-da algoritmlarni vizuallashtirish uchun prototip kutubxonasini yaratish juda qiziqarli va foydali loyiha bo'lishi mumkin. Bunday kutubxona orqali siz turli xil algoritmlarni, masalan, tartiblash, qidirish, graf algoritmlari va boshqalarni vizual ko‘rinishda taqdim etishingiz mumkin. Quyida oddiy bir prototip kutubxonasini yaratish uchun qadamlar keltirilgan:

    ### 1. Kutubxona Tuzilishi

    Dastlabki tuzilishini belgilaymiz. Misol uchun, `visualizer.py` nomli faylda asosiy funksiyalarni kiritamiz.

    ### 2. Kutubxona uchun Asosiy Funktsiyalar

    Keling, oddiy tartiblash algoritmini (masalan, Bubble Sort) vizuallashtirish uchun kod yozamiz.

    ```python

    import matplotlib.pyplot as plt

    import time

    def bubble_sort_visualization(arr):

    n = len(arr)

    for i in range(n):

    for j in range(0, n-i-1):

    if arr[j] > arr[j+1]:

    arr[j], arr[j+1] = arr[j+1], arr[j]

    # Vizuallashtirish

    plt.clf() # Oyna tozalash

    plt.bar(range(len(arr)), arr)

    plt.title("Bubble Sort Visualization")

    plt.pause(0.1) # Vaqtni kutish

    plt.show()

    # Misol uchun foydalanish

    if __name__ == "__main__":

    data = [64, 34, 25, 12, 22, 11, 90]

    plt.ion() # Interaktiv rejimga o'tish

    bubble_sort_visualization(data)

    ```

    ### 3. Kutubxonani Kengaytirish

    Bu kodda `matplotlib` kutubxonasi yordamida Bubble Sort algoritmini vizuallashtirdik. Siz ushbu asosiy kodga boshqa algoritmlarni qo'shishingiz, foydalanuvchilar uchun interfeys yaratishingiz yoki turli xil ko'rinishlar qo'shishingiz mumkin.

    ### 4. Qo'shimcha Funksiyalar

    Siz kutubxonaning imkoniyatlarini kengaytirishingiz mumkin:

    - **Turli algoritmlar**: Quick Sort, Merge Sort, Dijkstra algoritmi va boshqalar.

    - **Interaktiv interfeys**: Foydalanuvchilarga o'z ma'lumotlarini kiritish va algoritmni tanlash imkoniyatini berish.

    - **Ovoz qo'shish**: Algoritm harakatlari bilan birga ovoz effektlari qo'shish.

    ### 5. O'rnatish

    Agar siz bu kutubxonani boshqa loyihalarda ishlatmoqchi bo'lsangiz, uni `setup.py` fayli orqali paketga aylantirishingiz mumkin.

    ### 6. Foydalanish

    Yuqoridagi kodni `visualizer.py` faylida saqlang va terminalda quyidagi buyruqni bajarib ishga tushiring:

    ```bash

    python visualizer.py

    ```

    Bu oddiy prototip kutubxonasi sizga Python-da algoritmlarni vizuallashtirishni o’rganish va o’z g'oyalaringizni amalga oshirishda yordam beradi. Agar sizda qo'shimcha savollar yoki aniqroq talablar bo'lsa, iltimos, so'rang!


  1. belonesox
    07.10.2024 04:16

    Я при преподавании использую свою библиотеку «прозрачной визуализации отладки», https://github.com/belonesox/pyalgovisualizer (введение в разработку на ней тут → https://gitlab.ispras.ru/discopal/algo-visual/-/blob/master/contribution.md), вот короткий (10мин) доклад https://0x1.tv/20240629H (там идея, демо, и в частности — почему не manim не панацея), вот некоторые примеры → https://vimeo.com/showcase/10185384 (но смысл не в видеороликах, а в том, что студент в браузере может интерактивно разбираться в алгоритме).