Jupyter Notebook – это крайне удобный инструмент для разработчика. Являясь дата-инженером, я использую его как основную IDE. Единственным его ограничением является невозможность создания графических форм классическими методами, принятыми в Python. В этой статья я хочу поделиться лучшим способом решить эту проблему.
Основным достоинством Jupyter Notebook является то, что каждая ячейка представляет из себя отдельную программу, которая способна исполниться, как независимо от других ячеек, так и взять переменные из уже выполненных ячеек. Данное достоинство позволило стать юпитеру самым удобным инструментом для аналитика данных. И дальнейшее развитие этого инструмента именно в таком ключе привело к созданию библиотеки ipywidgets, основная задача которой создать в рамках ноутбуков инструменты визуализации и интерактивности. Предоставляя инструменты в виде элементов управления (кнопок, чекбоксов, слайдеров и т.д.), контейнеров для медиа и инструмента для создания графиков на основе Matplotlib, ipywidgets стал лучшим инструментом для визуализации работы с данными. Однако, ipywidgets – это не просто инструмент для создания интерактивности в ячейках, но и полноценная система для построения клиентских приложений внутри Jupyter Notebook, которая во многом схожа с WPF или Swing. Именно эту сторону библиотеки я постараюсь сегодня раскрыть.
Для демонстрации возможностей ipywidgets я создал совсем небольшое приложение состоящее всего из трех форм:
Первая форма – это главное меню, с которого можно перейти на две другие.
Вторая форма – форма на которой можно посмотреть список товаров, которые есть в базе данных. В базе данных сохраняется только пара с именем какого-то продукта и количества его на складе. В качестве БД, я использовал Redis, развернутый в докере.
На этой форме можно увидеть очень серьезную проблему ipywidgets – это отсутствие чего-то вроде GridView. Из-за этого создавать красивые табличные представления стандартными инструментами практически невозможно (справедливости ради есть второй, более сложный, но и более внешне привлекательный способ решения этой задачи с точки зрения эстетической красоты. Он описан по ссылке. Однако, на практике в промышленных задачах использовался только первый способ – выравнивание с помощью f-string).
Также можно заметить, что на форме нет кнопки "Назад". Это еще одна проблема ipywidgets, о которой я расскажу чуть позже. Можно заметить, что данная форма открылась в отдельной вкладке. Это один из наиболее удобных способов управлять связанными формами.
Третья форма – это форма, на которой можно ввести количество товара и его количество, после чего сохранить эту информацию в БД. Приложение сообщит о том, что сохранение прошло успешно сообщением под кнопкой “Сохранить”.
Как создать такое приложение – я и расскажу в этой статье, а также немного поделюсь опытом коммерческой разработки крупного приложения на этой технологии.
Шаг 1. Установка библиотек и создание первой формы
Для работы с ipywidgets понадобится всего две библиотеки: сама ipywidgets и оболочка IPython. Они устанавливаются через pip install.
Нужно понимать, что для запуска нужен Jupyter Notebook. Без него приложения работать не будут и программа просто выведет в консоли кортеж, в котором находятся объекты элементов на первой форме:
Установить Jupyter, можно через pip install или в докере, или с помощью плагина для VS Code.
Перед началом разработки, стоит решить – что будет использоваться в качестве контейнера форм. Как я написал выше, очень удобно использовать вкладки (класс Tab), однако, они могут стать большой проблемой в случае, если в приложении предполагается большое количество переходов между формами и возвратов на предыдущую форму. Связано это с тем, что по сути все вкладки хранятся в кортеже и для того, чтобы добавить новую вкладку, нужно пересоздавать кортеж с новой (об этом чуть позже) и точно так же пересоздавать кортеж без удаляемой вкладки, которую еще нужно найти в кортеже, что практически невозможно при большом количестве вкладок. Есть альтернативный вариант с использованием представлений HBox или VBox. Несмотря на то, что этот подход наиболее похож на подходы, используемые в Swing и WinForms, он порождает проблему с возвратом на предыдущую форму, если она должна хранить какое-то состояние. Именно это является проблемой реализации кнопки «Назад». На практике, в больших приложениях дебаг этой кнопки с очень большой вероятностью превратиться в ад. Именно поэтому я в своем опыте и данном приложении использую вкладки, так как это наиболее простой способ перемещаться на более старые формы. Более того, если вкладка не будет закрыта, то ее состояние сохранится до конца работы приложения, даже если были открыты несколько таких форм в других вкладках.
Перейдем к созданию формы главного меню.
1) Импортируем необходимые библиотеки:
import ipywidgets as widgets
from IPython.display import display
import redis
2) Для начала нам необходимо создать нашу систему вкладок:
self.__tabs = widgets.Tab(layout={'width': '600px'})
Она представляет из себя виджет Tab, который содержит параметр children и который является кортежем, содержащим все существующие вкладки. В данном приложении при создании объекта Tab мы не указываем ни одной вкладки.
Передаваемый параметр layout определяет размещение и размеры объекта. В данном случае задается ширина вкладки.
3) Кнопки создаются следующим образом:
list_btn = widgets.Button(description="Просмотр товаров")
На кнопку необходимо повесить обсервер, который будет реагировать на нажатие на кнопку:
list_btn.on_click(self.__list_form)
Это делается с помощью метода on_click, ему передается другой метод: в данном случае это метод, отрисовывающий требуемую форму. Необходимо учесть, что в указанный метод передается *args.
def __list_form(self, *args)
Аналогичным способом создаем и вторую кнопку.
1) Для отображения всех элементов на форме в едином контейнере, необходимо этот контейнер создать, делается это следующим образом:
box = widgets.VBox([
widgets.Label("Выберите пункт меню:"),
list_btn, add_btn
])
Контейнер представляет из себя объект класса, содержащий в себе список виджетов. Можно заметить, что прямо внутри создания контейнера был создан Label.
2) Теперь необходимо поместить контейнер во вкладку:
self.__tabs.children = [box]
self.__tabs.set_title(0, "Главное меню")
Для этого достаточно просто заполнить параметр children, который был оставлен, пустым списком, содержащим первую форму. Методом set_title задается порядок отображения текущей вкладки и ее имя.
3) Напоследок необходимо отобразить систему кладок на экране с помощью
IPython.display.display()
display(self.__tabs)
Отображение происходит один раз на все время работы приложения.
Шаг 2. Создание формы просмотра данных из БД
Как я писал выше, в ipywidgets нет способа создать красивое табличное представление без использования сторонних библиотек. Для этих целей обычно используется виджет SelectMultiple, который построчно выводит элементы списка. У виджета нет своей «шапки», поэтому ее придется создать как обычную строку первым элементом списка.
product_list = ["Товар | Количество"]
А сами элементы списка заполняются из Redis. Главное помнить, что каждая строка в виджете – это элемент списка. Сам виджет создается следующим образом:
sm = widgets.SelectMultiple(options=product_list)
Дальше аналогично нужно создать контейнер, но способ помещения контейнера в систему вкладок представляет из себя некий «костыль». Как я писал выше, в параметре children хранится кортеж, а кортежи в Python, как и элементы в них – неизменяемы. Поэтому необходимо преобразовать текущий кортеж в список, добавить к нему новый контейнер и снова привести все к кортежу. Также, чтобы данная вкладка не заменяла меню, необходимо установить ей индекс, следующий за индексом формы, который ее вызывает. Если указать меньший, то контейнер на форме с этим индексом перезапишется, а если больший, то приложение выдаст ошибку из-за выхода за границы кортежа и не откроет форму.
self.__tabs.children = tuple(list(self.__tabs.children) + [box])
self.__tabs.set_title(1, "Просмотр товаров")
Шаг 3. Создание формы добавления в БД
Заключительной формой станет форма добавления данных в БД. Она состоит из двух textbox, кнопки и изначально скрытого лейбла.
Textbox представляет из себя объект Text. Параметр description задает текстовый лейбл, который будет располагаться слева от поля. Если текст лейбла обрезается, то в параметре layout можно увеличить ширину виджета:
name_box = widgets.Text(description='Название: ')
Таким же способом можно создать textbox и для ввода количества, однако, числовое значение и его имеет смысл защитить от ввода чего-либо, кроме цифр, точки и минуса. Для этого в ipywidgets есть виджет FloatText. Его создание выглядит аналогичным образом:
quantity_box = widgets.FloatText(description='Количество: ')
Стоит отметить, что так же существует виждет IntText, кроторый запрещает ввод чисел с плавающей точкой и виджеты BoundedIntText и BoundedFloatText, которые отличаются от стандартных тем, что позволяют указать допустимый диапазон.
С создание кнопок уже происходило ранее, поэтому последним интересным элементом будет создание изначально скрытого элемента. Каждый виджет содержит параметр layout, который в свою очередь содержит флаг visibility. Для того, чтобы элемент был скрыт, его необходимо указать как hidden. Чтобы показать элемент, нужно указать параметр visible – соответственно.
save_label = widgets.Label(value="Сохранено!")
save_label.layout.visibility = "hidden"
save_label.layout.visibility = "visible"
Заключение
В данной статье я попытался рассказать о библиотеке ipywidgets как о средстве для создания графических приложений на базе Jupyter Notebook. Эту концепцию я продемонстрировал, создав приложение, которое выполняет две конкретные задачи и обращается к базе данных. Также я постарался рассказать о базовых виджетах, которые содержаться в библиотеке. Но их значительно больше – полный список можно посмотреть по ссылке. Кроме того, библиотека имеет поддержку HTML и CSS, что делает возможности по дизайну практически безграничными и дает возможность переопределять и создавать новые виджеты. В интернете можно найти кастомные виджеты практически на любые задачи, в том числе и красивые табличные представления.
Полный код программы
class App():
def __init__(self):
self.__tabs = widgets.Tab(layout={'width': '600px'})
def run(self):
list_btn = widgets.Button(description="Просмотр товаров")
list_btn.on_click(self.__list_form)
add_btn = widgets.Button(description="Добавление товара")
add_btn.on_click(self.__add_form)
box = widgets.VBox([
widgets.Label("Выберите пункт меню:"),
list_btn, add_btn
])
self.__tabs.children = [box]
self.__tabs.set_title(0, "Главное меню")
display(self.__tabs)
def __list_form(self, *args):
pool = redis.ConnectionPool(host='localhost', port=6379, db=0)
r = redis.Redis(connection_pool=pool)
product_list = ["Товар | Количество"]
for key in r.scan_iter():
key = key.decode('utf-8')
quantity = r.get(key).decode('utf-8')
product_list.append(key+ " |" + quantity)
sm = widgets.SelectMultiple(options=product_list)
box = widgets.VBox([
widgets.Label("Список товаров:"),
sm
])
self.__tabs.children = tuple(list(self.__tabs.children) + [box])
self.__tabs.set_title(1, "Просмотр товаров")
def __add_form(self, *args):
name_box = widgets.Text(description='Название: ')
quantity_box = widgets.FloatText(description='Количество: ')
save_btn = widgets.Button(description='Сохранить')
def __on_save_btn_click(*args):
pool = redis.ConnectionPool(host='docker.for.windows.localhost', port=6379, db=0)
r = redis.Redis(connection_pool=pool)
r.set(name_box.value, quantity_box.value)
save_label.layout.visibility = "visible"
save_btn.on_click(__on_save_btn_click)
save_label = widgets.Label(value="Сохранено!")
save_label.layout.visibility = "hidden"
box = widgets.VBox([
widgets.Label("Заполните поля:"),
name_box,
quantity_box,
save_btn,
save_label
])
self.__tabs.children = tuple(list(self.__tabs.children) + [box])
self.__tabs.set_title(1, "Добавление товаров")
dimas062
Какое-то изобретение велосипеда.