Литературное программирование

В далекие 80-ые годы прошлого века Дональдом Кнутом была выдвинута концепция литературного программирования (literate programming), перевернувшая привычное соотношение исходного кода, его выполнения и документирования: первичен человек и повествование, вторичен компьютер и преобразование исходного кода в исполняемый [1, 2]. Программа — это эссе для людей, в которое вставлены фрагменты кода. Инструментально эта идея была впервые реализована в системах WEB/CWEB с двумя ключевыми процессами: получение красиво форматированного документа для чтения и собранного из тех же фрагментов исполняемого исходного кода [3].

За прошедшие десятилетия эта концепция глубоко проникла в повседневную практику в системы научно-технических расчетов, включая Mathematica, MATLAB, Octave, Maple, Mathcad, Smath, Jupyter (Jupyter Lab (JL), Jupyter Notebook (JN)), Google Colab, Pluto.jl а также сравнительно новых проектов, Solara и marimo. Все они в той или иной степени реализуют метафору школьной рабочей тетради (блокнота), в которой описываются условия поставленной задачи с формулами, рисунками, а иногда с видео и эмодзи на полях, в них выполняются вычисления, результаты представляются в табличной и графической форме, делаются выводы…

Речь здесь пойдет о фреймворке marimo [4–6], названного в честь аквариумной водоросли, первый коммит на GitHub был осуществлен 14.08.2023. Разработчики проекта позиционирует его прежде всего как инструмент для работы с данными, но он интересен и для поддержки технологии STEM (Science, Technology, Engineering, Mathematics – наука, технологии, инженерное дело и математика) в учебном процессе. marimo является одной из блокнотных технологий, работающих в браузере. Многие идеи разработчики почерпнули из Ploto.jl – блокнотной технологии для Julia [7].

Основные особенности marimo

  • Реактивность. Отсутствие скрытых состояний. Автоматическая поддержка зависимостей между ячейками блокнота и обусловленный этим детерминированный порядок выполнения вычислений.

  • Блокноты – чистый Python. Возможность работы с блокнотами как с модулями Python, в том числе импорт объектов блокнотов. Как следствие упрощение работы с Git и фреймворками тестирования.

  • Современная среда разработки, включающая в себя мощный встроенный редактор с удобным автодополнением, ассистентами ИИ, средства форматирования исходного кода, просмотра значений переменных и источников данных в процессе выполнения, зависимостей между объектами блокнота, менеджер пакетов с поддержкой uv, pip, poetry, conda.

  • Динамический Markdown – возможность встраивания в текст исполняемых фрагментов.

  • Расширенные возможности работы с данными. В ячейках SQL можно делать запросы к базам и фреймам данных, имеются специальные виджеты для просмотра результатов запросов.

  • Возможность построения интерактивных приложений без функций обратного вызова. Интерактивное построение раскладки приложения на сетке в редакторе блокнота.

  • В marimo из коробки имеются средства для различных представлений блокнотов: редактирование исходного кода, выполнение (исходный код ячеек не отображается), презентация (ячейки блокнота становятся слайдами и листаются). Режимы представления легко переключаются в процессе разработки.

  • Гибкие средства развертывания, начиная от запуска приложения marimo из командной строки, через встраивание в статическую веб-страницу, публикацию на облачном сервисе marimo.app до интеграции в приложения FastAPI [8] или FastHTML [9].

Установка и запуск marimo

Установка marimo осуществляется любым менеджером пакетов Python, например с помощью pip

pip install marimo[sql] 

или uv

uv add marimo[sql]

Естественно, перед этим необходимо создать виртуальное окружение.

Основной режим работы с блокнотом marimo – это редактирование

marimo edit путь_к_блокноту.py

В свою очередь выполнить блокнот можно командой

marimo run путь_к_блокноту.py

При этом исходный текст в ячейках блокнота не будет отображаться. В режиме редактирования переключиться в режим выполнения и назад можно по сочетанию клавиш Ctrl+.

Дополнительно можно познакомиться с интерактивными руководствами по marimo

marimo tutorial наименование_руководства

Перечень руководств выводится командой

marimo tutorial

Реактивность

Это основное отличие marimo от Jupyter. Все переменные с областью видимости блокнота является реактивными. Если в ячейке изменить значение переменной, то это приведет к перевычислению всех выражений и/или вызову функций, в которых есть ссылки на данную переменную. Изменять значение переменной можно только в ячейке, где ей впервые было присвоено значение.

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

Реактивность в ячейках блокнота marimo
Реактивность в ячейках блокнота marimo

Перемещение движка слайдера во второй ячейке приводит к изменению значения, выводимого в первой ячейке. Кстати, область вывода находится над ячейкой, а не под ней, как в Jupyter, хотя это можно изменить в настройках блокнота. Точно также можно запретить автоматическое выполнение отдельных ячеек.

Наряду с реактивными можно использовать и локальные на уровне ячейки переменные, их имена начинаются с символа подчеркивания. Данный подход к реактивности снимает проблему скрытых состояний и воспроизводимости в Jupyter, когда в какой-либо ячейке задано значение переменной, используемой в других ячейках. Если выполнить и удалить ячейку с определением переменной в блокноте Jupyter, то повторное выполнение других ячеек ничего не изменит. При последующем перезапуске блокнота возникнет ошибка.

С другой стороны, автоматизация выполнения с помощью реактивных переменных приводит к накладным расходам при выполнении ресурсоёмких ячеек со ссылками на реактивные переменные.  

Невозможность переопределения реактивных переменных приводит к необходимости принудительного «обертывания» их в функции.

Блокнот marimo – «чистый» Python

В отличие от Jupyter, где блокноты кодируется в JSON, блокноты marimo – текстовые файлы с расширением .py. Структура ячеек блокнота поддерживается с помощью декораторов функций. В качестве примера приведем исходный текст блокнота из предыдущего раздела.

import marimo

__generated_with = "0.17.7"
app = marimo.App(width="medium")

with app.setup:
    # Ячейка настройки с экспортируемой функцией plot
    import numpy as np
    import matplotlib.pyplot as plt

    def plot(x, y, figsize=(3,3), lw=2):
        fig = plt.figure(figsize=figsize)
        for xx, yy in zip(x, y):
            plt.plot(xx, yy, lw=lw)
        return fig


@app.cell
def _():
    import marimo as mo
    return (mo,)


@app.cell
def _(a):
    # Ячейка 1
    a.value
    return


@app.cell
def _(mo):
    # Ячейка 2
    a = mo.ui.slider(
        start=1, stop=100, value=42, label="a:", step=1, show_value=True
    )
    a
    return (a,)


if __name__ == "__main__":
    app.run()

Мало того, что такой формат блокнотов радикально упрощает работу с системами контроля версий, но и позволяет осуществлять экспорт объектов блокнота, например, функций. Экспортируемые объекты должны быть определены на самом верхнем уровне блокнота с помощью выбора в меню блокнота Add setup cell (Добавить ячейку настройки). В ячейке настройки должны быть импортировано все необходимое для работы функций.

Типы ячеек. Динамический Markdown

В блокнотах marimo могут быть ячейки трех типов: Python, SQL и Markdown. В отличие от Jupyter, где кроме Python можно работать с более чем 30 языками программирования, включая Javascript, Julia, R, marimo поддерживает только Python. В ячейках SQL умалчиваемым источником данных является база данных DuckDB в оперативной памяти, но можно делать запросы к различным источникам данных, включая SQLite, фреймы данных pandas и polars.

Ниже на рисунке приводится пример приложения marimo для проведения контрольной по запросам SQL.

Приложение marimo —  контрольная по SQL
Приложение marimo — контрольная по SQL

По сравнению с Jupyter возможности Markdown в marimo расширены. Например, возможна вставка в Markdown подсвеченных исходных текстов Python, SQL

```sql
SELECT * FROM Users
```

Допускается использование диаграмм. Покажем, как подготовить интеллект-карту с оглавлением статьи в ячейке Python и вставить её в ячейку Markdown.

mindmap_text = '''
mindmap
root(marimo — не только аквариумная водоросль)
    Литературное программирование
    Установка и запуск marimo
    Реактивность
    Блокнот marimo – «чистый» Python
    Виды ячеек. Динамический Markdown
    Пользовательские интерфейсы в marimo
    Интеграция приложений marimo
    Заключение
    Литература 
'''
content = mo.mermaid(mindmap_text)

Для вставки в ячейку Markdown достаточно имя переменной заключить в фигурные скобки.

{content} 

Интеллект-карта отображается так

Интеллект-карта в ячейке Markdown
Интеллект-карта в ячейке Markdown

Кроме интеллект-карт можно работать с блок-схемами, диаграммами Ганта. Все это делает блокноты marimo удобным инструментом для подготовки к занятиям, тем более одним переключением в пользовательском интерфейсе блокнот можно превратить в полноценную интерактивную презентацию.

Пользовательские интерфейсы в marimo

Набор элементов пользовательского интерфейса (виджетов) построен на базе библиотеки anywidget [10] и не совместим с библиотекой ipywidgets, используемой в Jupyter. Набор виджетов стандартный, несколько скромнее чем в Jupyter, разработчики расширяют его в основном в направлении работы с данными. Виджеты реактивные, практически все имеют свойство value (значение), которое может быть изменено интерактивно пользователем или в исходном коде. Именно это позволяет строить пользовательские интерфейсы без функций обратного вызова. По умолчанию атрибут value виджета изменяется при любом действии пользователя, но можно связать изменение значения с потерей виджетом фокуса.

В виджетах имеются атрибуты on_change, а в некоторых on_click, их можно связать с функциями обратного вызова. Можно определить объекты состояния, позволяющие считывать и изменять его. Приведем пример из документации marimo синхронизации двух слайдеров.

# Ячейка 1. Состояние
get_state, set_state = mo.state(1.)

# Ячейка 2. Первый слайдер
s1 = mo.ui.slider(1., 10., 0.1, value=get_state(), 
                  on_change=set_state)

# Ячейка 3.Второй слайдер
s2 = mo.ui.slider(1., 10., 0.1, value=get_state(), 
                  on_change=set_state)

# Ячейка 4. Пользовательский интерфейс
mo.md(f'''
# Синхронизация состояния двух слайдеров
s1 {s1} {s1.value:5.1f} s2 {s2} {s2.value:5.1f}
''')

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

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

В marimo имеется набор виджетов-контейнеров, включая вертикальный и горизонтальный стеки, гармошку и вкладки, однако, в связи с тем, что виджеты разнесены по отдельным вкладкам, компоновать пользовательский интерфейс удобнее, перетаскивая ячейки, в которых отображаются виджеты, на сетке в одном из режимов редактора marimo.

marimo из коробки поддерживает визуализацию с помощью библиотек matplotlib, plotly и altair, последнюю разработчики рекомендуют для интерактивной визуализации. По ощущениям отрисовка в marimo осуществляется медленнее чем в блокнотах Jupyter. Ниже приводится пример приложения marimo, позволяющего вращать трехмерные фигуры относительно трех осей координат.

Визуализация в marimo
Визуализация в marimo

Интеграция приложений marimo

Как уже говорилось выше, приложения marimo можно непосредственно запускать из командной строки в режиме редактирования/разработки, презентации и рабочем. В последних двух случаях исходный код Python не отображается, а пользователь взаимодействует только с виджетами пользовательского интерфейса, встроенными в ячейки Markdown и Python.

Кроме того, приложения marimo интегрируются в веб-приложения, написанные на Flask, FastAPI, FastHTML. Ниже приводится пример интеграции двух приложений marimo в веб-приложение FastAPI.

from fastapi import FastAPI, Request, Response, Depends, HTTPException, status
from fastapi.responses import HTMLResponse
import marimo

app = FastAPI() # Cоздаем приложение FastAPI 

# Основная страница
@app.get("/")
async def home(request: Request):
    data = '''
   <!DOCTYPE html>
    <html>
    <head>
        <title>Интеграция приложений marimo в FastAPI</title>
    </head>
    <body>
        <h1>Интеграция приложений marimo в FastAPI</h1>
        <a href="/example" target="marimo">Первый пример приложения marimo (/example) </a>&nbsp; &nbsp;
        <a href="/example2"  target="marimo">Второй пример приложения marimo (/example2) </a><br/>
        <iframe src="/example" style="width:100%; height:85vh; border:none;">
        </iframe>
    </body>
    </html>
    '''
    return HTMLResponse(content=data) 

# Делаем приложения marimo доступными из FastAPI
mo_server = marimo.create_asgi_app()
mo_server = mo_server.with_app(path=f"/example",  root="example.py")
mo_server = mo_server.with_app(path=f"/example2",  root="a2_10.py")
app.mount("/", mo_server.build())

# Запускаем FastAPI
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="localhost", port=81)

Приложения marimo могут выполняться как в плавающем фрейме на странице FastAPI, так и в отдельных вкладках браузера. Это полезно для публикации коллекций интерактивных приложений marimo. Сборку коллекции можно осуществлять динамически, анализируя содержимое заданных папок на сервере. Кроме того, приложениям marimo при их запуске можно предавать данные из FastAPI.

Галерея приложений marimo, встроенных в приложение FastAPI
Галерея приложений marimo, встроенных в приложение FastAPI

Данная возможность оказывается очень удобной при вставке интерактивных приложений в электронные учебники и проведении контрольных.

В marimo встроена возможность преобразования приложения в статическую веб-страницу с помощью экспорта в WebAssemby (WASM). С такими приложениями marimo можно работать в веб-браузере без необходимости установки Python на локальном компьютере. Сохраняется возможность использования библиотек Python для научно-технических расчетов, включая numpy, pandas, scipy и matplotlib. Для преобразования достаточно выполнить команду

marimo export html-wasm marimo_app.py -o marimo_app.html

Если в блокноте нет ошибок,  то появится целевой html-файл и папка assets. Для запуска преобразованного приложения необходим веб-сервер. Для проверки работоспособности достаточно запустить встроенный веб-сервер командой

python -m http.server 
Статическая HTML-страница со встроенным приложением marimo
Статическая HTML-страница со встроенным приложением marimo

Для запуска приложения в адресной строке браузера нужно ввести

http://localhost:8000/marimo_app.html

Во вкладке браузера отобразится статическая веб-страница со встроенным приложением marimo, как показано на рисунке выше.

Следует отметить, что размер html-файла обычно невелик, в нашем случае 25 кб, а вот в папке ресурсов assets сгенерировано более 1000 файлов общим объем 33 Мб. Так что запускаются преобразованные приложения marimo через сеть не слишком быстро.

Заключение

Привлекательным в marimo является прежде всего возможность интеграции результатов вычислений, элементов пользовательского интерфейса в форматированный текст Markdown, что удобно при разработке интерактивных электронных учебников.

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

Немаловажным является то, что блокноты marimo представляют собой текстовые файлы Python, облегчая управления проектами и давая возможность импорта объектов блокнотов.

Как показывает опыт, освоение студентами работы с блокнотами marimo идет быстрее по сравнению Jupyter. Привлекательным является прозрачность порядка выполнения ячеек блокнота при проведении вычислительных экспериментов на практических занятиях. Недостатком по сравнению с Jupyter является отсутствие встроенного менеджера файлов для параллельного запуска нескольких блокнотов.

Наконец, блокноты marimo достаточно просто интегрируются в веб-приложения Flask, FastAPI и FastHTML, что в свою очередь упрощает построение интерактивных электронных учебников и виртуальных лабораторных работ. Сделать и поддерживать такую интеграцию в среде Jupyter существенно сложнее.

Литература

  1. Knuth, D. E. Literate Programming. The Computer Journal 27(2), 97–111 (1984). DOI: 10.1093/comjnl/27.2.97. 1. https://academic.oup.com/comjnl/article/27/2/97/354237

  2. Knuth, D. E. The WEB System of Structured Documentation. Stanford Computer Science Report STAN-CS-83-980 (1983). https://infolab.stanford.edu/TR/CS-TR-83-980.html

  3. Knuth, D. E., Levy, S. The CWEB System of Structured Documentation. Addison-Wesley (1994). Описание и материалы: https://www-cs-faculty.stanford.edu/~knuth/cweb.html

  4. The future of Python notebooks is here. https://marimo.io/

  5. Сравниваю Jupyter Notebook, Google Colab, Kaggle и Marimo глазами исследователя и начинающего Data Scientist. https://habr.com/ru/articles/969090/

  6. Marimo — как Jupyter, только лучше. https://pythontalk.olegtalks.ru/marimo_ide

  7. Pluto.jl — interactive Julia programming environment. https://plutojl.org/

  8. Любанович Б. FastAPI: веб-разработка на Python. — Астана: «Спринт Бук», 2024. — 288 с. ISBN 978-601-08-3847-5

  9. Modern web applications in pure Python. https://fastht.ml/

  10. anywidget. reusable interactive widgets made easy. https://anywidget.dev/

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