В этой публикации подробно рассказывается о моих первых детских шагах по объединению серверной экосистемы и выразительности Python, интуитивности Jupyter и интерактивности на стороне клиента Javascript для создания «идеального приложения для всего».
Позвольте мне сначала немного рассказать о конкретной мотивации того, что я пытаюсь сделать. На работе я довольно часто использую MapboxGL для визуализации больших данных на карте. Это отличная библиотека Javascript для отображения клиентских карт с ускорением на GPU с наложенными данными.
Основная цель нашего веб-приложения - сделать науку о геопространственных данных доступной для обычных людей. Хотя наши целевые аналитики могут быть экспертами по Excel, моя личная цель - заставить их использовать Jupyter Notebook, и с этой целью я стремлюсь интегрировать MapboxGL в Jupyter для улучшения качества интерактивных карт: проектировать данные с помощью Python / Pandas, а также легко отображать и анимировать их.
Райан Бауманн и его участники создали библиотеку mapboxgl-jupyter, но она ограничена тем, что карта, отображаемая в Jupyter, является статической, это просто ячейка, которая отображает iframe
со всеми данными для карты прямо в источнике iframe! Мое видение состоит в том, что по мере того, как пользователь изменяет данные во фрейме данных Pandas, карта должна обновляться реактивно, обеспечивая надлежащий исследовательский анализ и визуализацию тенденций и движения.
Вот как выглядит вывод библиотеки: iframe со встроенными данными:
<iframe id="map" ,="" srcdoc="<!DOCTYPE html> <html> <head> <title>mapboxgl-jupyter viz</title> <meta charset='UTF-8' /> <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> <script type='text/javascript' src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.41.0/mapbox-gl.js'></script> ... map.on('load', function() { map.addSource('data', { 'type': 'geojson', 'data': {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'properties': {'Avg Medicare Payments': 7678.21, 'Avg Covered Charges': 35247.03}, 'geometry': {'type': 'Point', 'coordinates': [-85.3629, 31.2162]}}, {'type': 'Feature', 'properties': {'Avg Medicare Payments': 5793.63, 'Avg Covered Charges': 16451.09}, 'geometry': {'type': 'Point', 'coordinates': [-88.1428, 32.453]}}, {'type': 'Feature', 'properties': {'Avg Medicare Payments': 7145.96, 'Avg Covered Charges': 36942.36}, 'geometry': {'type': 'Point', 'coordinates': [-87.6829, 34.7941]}}, [... etc!]
Однако то, что я здесь пытаюсь сделать, не ограничивается работой с MapboxGL. Это было бы в равной степени применимо для создания достойного плагина d3.js для Jupyter (вы можете представить себе, что matplotlib когда-нибудь будет вытеснен!) Или побочного проекта, которым я собираюсь заняться следующим: синтез WebAudio на базе за счет обработки чисел Python с помощью дружественного и интуитивно понятного интерфейса Jupyter (точно так же, если вы знаете какие-либо звуковые материалы, можете ли вы представить, что это вытесняет мощный, но сложный SuperCollider!)
Внутренности Jupyter
Мне нравится Jupyter, но, исследуя, как решить эту проблему, я с грустью обнаружил, что он использует JQuery и Backbone.js (правда!)
Мой первый подход заключался в создании расширения для записной книжки, но оказалось, что они больше предназначены для добавления в пользовательский интерфейс записной книжки. Я не мог найти простой способ связи с ядром Python, которое на самом деле выполнялось записной книжкой.
Именно тогда я наткнулся на виджеты. Во-первых, вам нужно включить расширение блокнота виджетов:
jupyter nbextension enable — py widgetsnbextension
Если расширение еще не установлено, conda install widgetsnbextension
следует исправить его.
Переменные Python, запускающие события Javascript
Это стало возможным благодаря чертам характера, с которыми я раньше не сталкивался. Traitlets - это библиотека для IPython, которая создает новые типы Python (на основе знакомых, таких как list, string, dict и т. Д.), Где переменные этих типов имеют дополнительные возможности: в частности, строгую проверку типов и, что наиболее важно для нашей цели: уведомления, обратные вызовы, которые происходят при изменении значения объекта. Трейтлеты основаны на библиотеке черт, которую я мог бы использовать, если бы мне когда-либо понадобилось создать настольное приложение с графическим интерфейсом пользователя на Python.
Сначала я начал с канонического примера Hello World для виджетов Jupyter. Но у него было два ограничения; во-первых, мне нужно использовать JSON для связи с картой (т. е. сопоставление между объектом Javascript и dict Python), а во-вторых, мне нужно, чтобы это был готовый к использованию модуль Python из коробки, магия Javascript победила » t сократить это.
(Мне никогда раньше не нужно было использовать магию Javascript в Jupyter, но оказалось, что это всего лишь код, выполняемый точно так, как если бы вы открыли инструменты разработчика Chrome и вставили его в консоль JS).
Встраивание Javascript в Python
На самом деле это не так уж сложно. Можно добавить на страницу произвольный HTML-код, включая Javascript, с помощью модуля IPython.core.display
. (Вы даже можете делать такие непослушные вещи, как window.open
).
from IPython.core.display import HTML HTML("<script>console.log('Hello, world!');</script>")
Не совсем Python как таковой, но он работает с любым ядром IPython / Jupyter. Более того, вы можете отказаться от тега script, импортировав вместо него Javascript:
from IPython.core.display import Javascript Javascript("alert('Hello, world!')")
Превращаем его в отдельный модуль
Я хочу, чтобы пользователь мог импортировать модуль, запускать некоторые команды точно так же, как обычно в Python, и получать выходную ячейку, которая изменяется, когда данные основаны на изменениях. Вот как это выглядит в моем приключении:
import ipywidgets as widgets class MapboxWidget(widgets.DOMWidget): from traitlets import Dict, Unicode _view_name = Unicode('MapboxGLView').tag(sync=True) _view_module = Unicode('mapboxglModule').tag(sync=True) _view_module_version = Unicode('0.1.0').tag(sync=True) value = Dict({"my_key": "Hello"}).tag(sync=True) def create(self): from IPython.core.display import Javascript return Javascript(""" require.undef('mapboxglModule'); define('mapboxglModule', ["@jupyter-widgets/base"], widgets => { let MapboxGLView = widgets.DOMWidgetView.extend({ defaults: _.extend({}, widgets.DOMWidgetModel.prototype.defaults, { value: '' }), render: function() { this.value_changed(); this.model.on('change:value', this.value_changed, this); }, value_changed: function() { this.el.textContent = this.model.get('value').my_key; }, }); return { MapboxGLView, }; }); """)
Там есть немного шаблонов, и JS не очень аккуратный, но цель, надеюсь, достаточно ясна.
Когда класс создается, он вводит некоторый JS в страницу Jupyter Notebook, которая использует AMD для определения mapboxglModule
и, в целом, представления, представленного на основе модели, описанной в коде Python. В этом простом случае это просто текстовый элемент, но он может быть сколь угодно сложным (в конечном итоге для меня добавлена карта MapboxGL).
Код также определяет свойство класса Python value
, которое является словарем. При изменении значения, на которое указывает my_key
, все, что отображается в выходной ячейке, будет автоматически, реактивно, обновляться!
Большая проблема: что-то где-то должно вернуть Javascript в качестве вывода, иначе это не будет выполнено в записной книжке. Вот почему есть отдельная функция create
. (К сожалению, это невозможно __init__
, потому что эта функция должна возвращать None). В конечном итоге я намерен предоставить размещенные записные книжки нашим пользователям через JupyterHub, и я бы, вероятно, внедрил модуль Javascript в пустой блокнот по умолчанию, вместо того, чтобы требовать от пользователя вызова кажущегося избыточным create функция.
Выход и использование
Еще одна проблема - мы используем объект dict / Javascript, на котором не будут обнаружены никакие изменения ключей - ссылка объекта указывает на то же место, поэтому он смотрит на traitlet так, как будто он не изменился. Вот почему я создал новый объект, а не просто изменил ключ существующего объекта. Сколько раз у меня были проблемы такого рода с объектами «глубокого наблюдения»? Очень много!
Как только это будет сделано, снова посмотрите на ячейку, отмеченную [75]:
Я больше не выполнял ячейку, не выполнял ее в Photoshop или какие-либо другие уловки - выходные данные ячейки буквально меняются прямо на ваших глазах.
Затем применяем этот принцип реактивности к карте Mapbox, d3, WebAudio, graphviz или networkx визуализации, а затем создаем одно бизнес-приложение, которое будет управлять ими всеми!