Создание прототипа библиотеки для визуализации алгоритмов на Python

от автора

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/848656/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *