Введение
Как вам идея создавать веб-приложения полностью на Python? Звучит заманчиво! Разработка веб-приложения требует владения навыками как фронтенда, так и бэкенда. В их число входят HTML, CSS, JavaScript, разные фреймворки, а также Python и другие серверные языки бэкенда. Как видно, предполагается большой объем работы!
А если бы для создания веб-приложения использовался только Python, теоретически даже один файл, то скорость разработки возросла бы в разы. В настоящее время существуют несколько библиотек, которые пытаются реализовать эту идею на практике. Самая популярная из них, судя по количеству звезд на GitHub, — это Streamlit. На момент написания статьи она была отмечена 24,9 тыс. звезд.
Несмотря на невероятную лаконичность Streamlit и способность создавать веб-приложения в несколько строк кода, у этой библиотеки есть и недостатки. Один из них — ограниченные возможности для создания пользовательских интерфейсов. Кроме того, следует отметить ее довольно странный механизм, при котором все перезапускается при каждом изменении состояния.
Не имею ничего против Streamlit. С удовольствием с ней работаю, если она способствует решению поставленных задач. Но вот для создания приложений со сложными интерфейсами и/или вложенными состояниями она не подходит.
Таким образом, для воплощения задуманного нужен новый, более настраиваемый фреймворк, который органично бы объединял гибкость фронтенд-фреймворков и Python.
Solara
Не так давно был представлен фреймворк Solara, предназначенный для создания веб-приложений на чистом Python. В его документации содержатся интересные улучшения Streamlit. К ним относятся вложенные переиспользуемые компоненты с собственными состояниями, не требующие повторного выполнения без необходимости, а также простая интеграция с Jupyter Notebook.
Особенно впечатляет тот факт, что официальный сайт Solara также создан с помощью фреймворка Solara, чего не скажешь о Streamlit. Предлагаем ссылки на официальный репозиторий GitHub и сайт Solara.
В теории все звучит прекрасно. Но как выглядит код? Так ли он хорош на практике? Помогает ли он эффективно создавать нужные приложения?
В следующих разделах статьи мы протестируем данный фреймворк. Для этого создадим что-нибудь одновременно простое, но при этом достаточно сложное, чтобы проверить его возможности, а именно приложение-планировщик задач todo app
. Я уже проводил подобный эксперимент с Shiny для Python, теперь настала очередь Solara. Изображение итогового результата:
В конце статьи продемонстрируем работу получившегося приложения. За дело!
Написание кода в Solara
Перед началом работы следует отметить пару моментов. Во-первых, фреймворк является совершенно новым, и сложно предсказать возможные изменения по мере его развития. Кроме того, я не претендую на звание эксперта Solara.
Начнем с определения глобальных переменных:
import solara
text_input = solara.reactive("")
todos = solara.reactive([
{ "text": solara.reactive("Learn Solara"), "done": solara.reactive(False) },
{ "text": solara.reactive("Build a Solara app"), "done": solara.reactive(False) }
])
Как видно, эта операция выполняется с помощью solara.reactive(...)
. Для строковых переменных просто добавляется строка в качестве начального значения. Для приложения-планировщика срабатывает добавление вложенных элементов словаря, также определенных с помощью solara.reactive
.
Page()
Затем определяем основной компонент с именем Page
:
@solara.component
def Page():
# добавление css
solara.Style("""
.add-button {
margin-right: 10px;
}
""")
# центрирование карты
with solara.Column(align="center"):
with solara.Card(title="Todo App"):
for todo in todos.value:
Todo(todo)
if len(todos.value) == 0:
solara.Text("No todos yet.")
solara.InputText(label="Add a todo", value=text_input),
solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),
Page()
Рассмотрим код шаг за шагом. Компонент определяется посредством декоратора @solara.component
. В данном случае это основной компонент, содержащий весь интерфейс. Сначала в компоненте вызывается solara.Style(...)
для добавления стилей CSS к одной из кнопок (обратите внимание на атрибуты classes=[..., “add-button”]
для этой кнопки). Не стоит волноваться, если вы не знакомы с CSS. Данная операция относится к разряду нетипичных, о чем и предупреждается в документации.
Примечание. Это операция считается функциональностью повышенного уровня сложности, требующей осторожного подхода.
Я намеренно ее использовал, чтобы протестировать способность фреймворка задействовать такие распространенные фронтенд-инструменты, как CSS.
Далее создаем solara.Column
для центрирования компонентов внутри и solara.Card
для создания стилизованного контейнера:
with solara.Column(align="center"):
with solara.Card(title="Todo App"):
...
На следующем этапе перебираем элементы в глобальном состоянии todos
, которое было определено ранее, и передаем каждое состояние в списке отдельному компоненту Todo
:
for todo in todos.value:
Todo(todo)
if len(todos.value) == 0:
solara.Text("No todos yet.")
Код для компонента Todo
рассмотрим позже.
Наконец, код предусматривает текстовое поле для ввода задач и 2 кнопки: одна для добавления новых задач, а другая для удаления выполненных.
solara.InputText(label="Add a todo", value=text_input),
solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),
Для текстового поля значение устанавливается в одно из глобальных состояний, которое автоматически обновляется при потери элементом фокуса (или при каждом нажатии клавиши совместно с continuous_update=True
).
Обе кнопки располагают обратными вызовами on_click
для обработки логики при нажатии. Рассмотрим код:
def on_add_todo():
todos.set(todos.value + [{
"text": solara.Reactive(text_input.value),
"done": solara.Reactive(False)
}])
text_input.set("")
def clear_finished_todos():
todos.set([todo for todo in todos.value if not todo["done"].value])
Первый метод on_add_todo
выполняет конкатенацию старых элементов todo
с новым элементом, который принимает значение внутри текстового поля, а затем его очищает. Обратите внимание, что переменные todos
и text_input
являются объектами Reactive
(определенными ранее с помощью solara.Reactive(...)
), которые обрабатывают логику, необходимую для состояний. Таким образом, для доступа к их текущим фактическим значениям требуется задействовать метод доступа .value
.
Второй метод clear_finished_todos
перебирает элементы todo
и удаляет те, которые выполнены, т.е. “done”
. Поскольку элемент словаря done
также является объектом Reactive
, доступ к его значению осуществляется посредством .value
.
Todo()
Последний фрагмент мозаики — функция Todo
, переиспользуемый компонент, с помощью которого отображается каждый элемент todo
. Ниже представлен код:
@solara.component
def Todo(todo):
# определение локального состояния, только для данного компонента
editing, set_editing = solara.use_state(False)
# размер 0 будет занимать минимум места
with solara.Columns([1, 0]):
# установка фонового цвета в зависимости от состояния done
color = "#d6ffd6" if todo["done"].value else "initial"
# css для придания привлекательного внешнего вида
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
# если редактирование (editing) является true, элемент редактируется
if editing:
solara.InputText(label="Edit todo", value=todo["text"])
else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])
# кнопки для редактирования/сохранения и удаления
solara.Column(children=[
(
solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
if editing
else
solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
),
solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
])
Сначала вызывается solara.use_state(<initial value>)
для определения локального состояния внутри компонента. Если вы работали с функциональными компонентами в ReactJS, то эта процедура вам знакома.
Далее определяем 2 столбца:
with solara.Columns([1, 0]):
...
Один из них должен растягиваться (текст), а другой — быть минимального размера (кнопки).
Текст отображается следующим образом:
# установка фонового цвета в зависимости от состояния done
color = "#d6ffd6" if todo["done"].value else "initial"
# css для придания привлекательного внешнего вида
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
# если редактирование (editing) является true, элемент редактируется
if editing:
solara.InputText(label="Edit todo", value=todo["text"])
else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])
Добавляем стиль CSS background-color
в зависимости от состояния done
вместе с фиксированными стилями CSS. Затем с учетом состояния editing
содержимое подгоняется либо под флажок (checkbox), либо поле для ввода текста. Флажок, по аналогии с текстовым полем, обновляет value
объекта Reactive
при нажатии.
И наконец, обзаводимся кнопками в виде иконок, расположенными вертикально:
# кнопки для редактирования/сохранения и удаления
solara.Column(children=[
(
solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
if editing
else
solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
),
solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
])
При редактировании отображается кнопка save
, в противном случае — кнопка edit
. Кроме того, имеется кнопка delete
для удаления текущих задач.
Готово! Посмотрим, что получилось:
Обратите внимание, что при нажатии на флажок в компоненте Todo
, обновляется только компонент Todo
, а не компонент Page
. Такой подход позволяет создавать более эффективные программы.
Полный вариант кода:
import solara
text_input = solara.reactive("")
todos = solara.reactive([
{ "text": solara.reactive("Learn Solara"), "done": solara.reactive(False) },
{ "text": solara.reactive("Build a Solara app"), "done": solara.reactive(False) }
])
def on_add_todo():
todos.set(todos.value + [{
"text": solara.Reactive(text_input.value),
"done": solara.Reactive(False)
}])
text_input.set("")
def clear_finished_todos():
todos.set([todo for todo in todos.value if not todo["done"].value])
@solara.component
def Todo(todo):
# определение локального состояния только для данного компонента
editing, set_editing = solara.use_state(False)
# размер 0 будет занимать минимум места
with solara.Columns([1, 0]):
# установка фонового цвета в зависимости от состояния done
color = "#d6ffd6" if todo["done"].value else "initial"
# css для придания привлекательного внешнего вида
with solara.Column(style=f"padding: 1em; width: 400px; background-color: {color};"):
# если редактирование является true, элемент редактируется
if editing:
solara.InputText(label="Edit todo", value=todo["text"])
else:
solara.Checkbox(label=todo["text"].value, value=todo["done"])
# кнопки для редактирования/сохранения и удаления
solara.Column(children=[
(
solara.IconButton(icon_name="save", on_click=lambda: set_editing(False))
if editing
else
solara.IconButton(icon_name="edit", on_click=lambda: set_editing(True))
),
solara.IconButton(icon_name="delete", on_click=lambda: todos.set([t for t in todos.value if t != todo]))
])
@solara.component
def Page():
solara.Style("""
.add-button {
margin-right: 10px;
}
""")
# to центрирование карты
with solara.Column(align="center"):
with solara.Card(title="Todo App"):
for todo in todos.value:
Todo(todo)
if len(todos.value) == 0:
solara.Text("No todos yet.")
solara.InputText(label="Add a todo", value=text_input),
solara.Button("Add", on_click=on_add_todo, classes=["primary", "add-button"]),
solara.Button("Remove finished tasks", classes=["secondary"], on_click=clear_finished_todos),
Page()
Запуск приложения Solara в Jupyter Notebook
Этот код можно легко запустить в Jupyter Notebook. Для этого просто вставляем код в ячейку и запускаем его:
Заключение
Тестирование фреймворка Solara позволило обнаружить в нем ряд преимуществ, отсутствующих в Streamlit. Среди них — переиспользуемые компоненты, усложненная функциональность с локальными и глобальными состояниями, а также более широкие возможности настройки, например с CSS.
Однако выявились и недочеты. Во-первых, хотелось бы задействовать базовые HTML
-компоненты. На данный момент можно вызвать solara.HTML(...)
, но только для изолированного рендеринга HTML
с единственной возможностью чтения. Обернуть в них другие компоненты уже не получится. Помимо этого, отсутствуют и другие базовые компоненты. Например, нет многострочных текстовых полей, хотя вероятно их появление в ближайшем будущем.
В целом, фреймворк Solara обладает большим потенциалом, и в моей практике он придет на смену Streamlit. Но в настоящее время он не готов заменить ReactJS на стороне клиента и Python на стороне сервера в ситуациях, требующих исключительной гибкости.
Читайте также:
- Как легко и быстро создать веб-приложение на базе МО с помощью Python
- Как быстро создать и развернуть веб-приложение на Python
- Управление памятью в Python: 3 частых вопроса на собеседовании
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jacob Ferus: Web Apps in Python with Solara — A Streamlit Killer?