🐍

ブラウザで動かすPythonの出来ること出来ないこと

2022/06/19に公開

はじめに

最近はPythonがブラウザで動くようになってきています。
この前はscriptタグにPythonを書くPyScriptが話題になっていましたね。

ただ、現状まだ発展途上で出来ることと出来ないことが混在しています。
そこで、2022/06時点での状況をまとめたいと思います

ちなみに、この記事の内容は個人開発でブラウザ上で動くPythonのPlayground
https://codepiece.pages.dev/?state=N4Igxg9gJgpiBcIDEACACgTwC4AsIDtBuV0HmFQM8VAwF0EuGQEoZA1hksCuGQOwY0BJAGScFaGQZ4ZB1hkDXDIHMGQJoMAHXypAZ7qAoOSYAlAK75ASQyAeoxGALBgDqAQQDKAWSH1AZQyA2hkChikMD52oBkIwKoMgGIYATjADOigDZYAdAAd8AHNAGQZAfFdABCNnQD8GcUkUQAVfQBzzQHsGQGflJkFAAYZTHiFALo9AWKihQFkGWIlUQGaGQEOGHkAJhkAihlM6lRTALQZAdQZAMwYlfBFAaIZNQGCNQBlXQC3fQAMGQEUGQF6jQC%2FFAcBlBk0Q0vL8ADMXCABbdA4UAEsd%2FwgXLBRWHYBDIJgAGkubu4AxQ58YFwlj25gUAF4nr9fBB%2FDB8AAKADkXnB1wCwShAEoJN9nn9AT87r5IPgAG6fLDQ9jIipHdEA8nAjbvLCfCFXX5vD4uXwvVgAOQAIgB9ACiXIA4nz9Cj8FiYL53NcCdC3J4fAigqT8CB7iB%2FNcwABrX7uBAAbQ17y8EAA7iAALrqrDXFx3LAIEC%2BAD08u8fkCQTVICg5vwpuuUH18ANoE1uCdsPw8K9PsULi8TpwWCw%2Fnc8BdbuuZt8QUOuEUACNFO5Pri6fg%2FJAdi6dodtRAvNcvBgy%2FhGy7o9c69d3HSXF24UqfdqYBgEABGAC%2BlunQA%3D
を作っていて得た知見をもとに作成しています

ブラウザで動くPythonのアーキテクチャーとコア技術

PyScriptのアーキテクチャーは上図のように3層になっています。

下から

  • WASM:ブラウザ[1]で動くバイナリコード。JavaScript以外の言語でも、WASMにコンパイルすることでブラウザで実行可能になる
  • Pyodide: CPython[2]をEmscripten(C/C++のWASMコンパイラ)でWASMにコンパイルしたもの。つまりブラウザでPythonを動かしているのはPyodide
  • PyScript: Pyodideをより使いやすくしたもの。後で詳しく説明しますが、PyodideでPythonのコードを実行しようとすると、Jsの中にPythonコードを書く形になり、扱いずらいです
pyodide.runPython(`print("hello world")`)

ブラウザでPythonを動かす際の制約はWASM・Pyodideに由来しており、PyScriptはあくまでPyodideのラッパーといえます。

この記事ではPyodideでできること、できないことを重点的に解説していきます。
WASMについてはその中で必要な個所だけ触れていきます

PyScriptの解説はいい記事を見つけたのでこちらをご参照ください

Pyodideの出来ること

ブラウザ上でpythonコードの実行

<script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script>でPyodideをCDNからロードして使うことができます。pythonのコードをPyodideのrunPythonの引数に文字列で渡して実行することができます

Pythonのコードを実行し、jsで戻り値を取得できます

CodePenのURLが不正です

逆にjsをPython側で実行もできます

CodePenのURLが不正です

import jsでjsのグローバル変数にPython側でアクセスできます。グローバル変数から辿れる関数を実行できます

pythonで書いた関数をjs側に渡してjs側で実行

CodePenのURLが不正です

なお、Js <-> Python のやり取りの際に型の変換が行われます。

細かい変換はドキュメントで確認できます

ブラウザ内にファイルシステム

Emscriptenの機能で疑似的なファイルシステムをブラウザ内に作ってくれます。
デフォルトではメモリ上に作られるため揮発的ですが、ブラウザのストレージに保存して永続化もできるようです

CodePenのURLが不正です

ブラウザ内にpip installで動的にパッケージインストール

ブラウザの中にpip installで動的にインストールするというのは個人的には衝撃的でした。
Cで書かれたパッケージは事前コンパイルとPyodideへの登録が必要ですが、
純粋にPythonで書かれたパッケージでwheelのあるものはPyPIから直接インストールすることができます。

PyodideでWASMにコンパイル済みのもの

pyodide.loadPackageをjs側で実行すればインストールできます

CodePenのURLが不正です

PyPIのパッケージ

micropipというパッケージを使うとPyPIからインストールできます

CodePenのURLが不正です

Pyodideの出来ないこと

C/C++で書かれたパッケージの動的インストール

WASMコンパイルしないといけないため、当然できません。
事前にコンパイルしてPyodideに登録する必要があります

関数呼び出し回数上限の変更

WASM側のコールスタックサイズの問題で、Python側の関数呼び出し上限設定を大きくしても小さな呼び出し回数でエラーを起こしてしまいます
https://github.com/pyodide/pyodide/issues/441

スレッド

Emscriptenではサポートされており、将来的に実装される見込みです
https://github.com/pyodide/pyodide/issues/237

同期的なsleep(同期IO)

ブラウザは基本的にシングルスレッドなため、sleepする手段がありません。
そのためtime.sleepはデフォルトでは何もしない関数になっており、sleepせずに即座に制御を返してきます。

async-awaitを使えば疑似的なsleepはできますが、そのためにはPythonのコードをasync-awaitで書く必要があります。

sleepの実現方法はこれまでいろいろな方法が議論がされていたようです。

このissueには次のことが書かれています

  • コードをすべてasync-awaitに書き換えてから実行する
  • AsyncifyというEmscripten の提供する機能を使い、WASM実行を非同期実行にする
    • 速度低下が著しいため却下された模様

また、ドキュメントにはWeb Worker上でAtomics.waitSharedArrayBufferを使った実行中断方法が書かれています

冒頭で挙げた私の作ったプレイグラウンドは試しにこのSharedArrayBufferでsleepを実装しています

このリンクで試せます

ただ、このSharedArrayBufferを使う方法はいろいろ難点があります。
ブラウザのセキュリティ上の問題で

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

というHTTPヘッダーをサーバーから返さないと使うことができません。

また、このHTTPヘッダーは例えばiframeの埋め込みでトラブルを起こします
iframe内では私の作ったサイトのヘッダーが適用されないようで、SharedArrayBufferが使えません。

sandbox設定だとさらに問題が起きます。
別オリジンのサイトのiframeの中から新しくページを開く際に、Cross-Origin-Opener-Policy: same-originによってアクセス拒否扱いされてしまいます。
この下記のnotion埋め込みで確認できます。🖊ボタンを押すとリンク先に飛ぶようにしていますが、アクセス拒否画面が出ます
https://vanilla-pilot-1ee.notion.site/Notion-Python-6a4e3fcd3ccd4a52b0364ab3732cd69b
chromeのNetworkタブの警告

ちなみに、sandboxでないiframeなら問題なく飛ぶことができます。はてなブログに直接iframeを書いた例です
https://zatdev.hatenablog.com/?_ga=2.110403973.1048556673.1655520457-111499327.1631459296

(zennに埋め込みたかったですがiframeをサポートしておらず・・・)

ソケット通信

ブラウザの中なので、ソケットを用意してもTCP/UDPの通信を外部へ送るすべがありません[3]

感想

PyodideはPythonとJsのコードが混ざってカオス

正直Pythonのコードを扱いやすいとは言えない現状です。
インデントはどうもよしなにやってくれるようでそこまで困らなかったのですが、シンタックスハイライト等が効かずなかなか読みづらいです。

また、発生する例外のトレースもJsとPythonが混ざって分かりづらくなってます。

こういうところがPyScriptのモチベーションなんだろうと思います

PyodideはjsでPythonのライブラリを使うためのものと思ってよさそう

機械学習で作ったAIやPythonの豊富なライブラリをブラウザで直接実行できる、というのが一番の用途なのではと感じました。

ちなみに画像処理のコードも載せておきます。
jsだとぱっとググった感じではあまりいい書き方が見つかりませんでしたが、pythonなら数行ですね
画像処理ライブラリPillowの実行例

Play Groundを作るには早すぎたかな・・・

Cを使うライブラリを動的にインストールできない点や、スレッド等が未実装でいろいろ動かせないためです。まあ作ってしまったので公開するのですが

まだまだ発展途上ではありますが、Pythonのライブラリをブラウザで使うにはいいツールだと思いました

脚注
  1. ブラウザだけでなく、サーバーもWASMで動かすものが出てきていますので興味がある人はぐぐってみてください。 ↩︎

  2. 普通PythonというとこのCPythonを指します。Cで書かれています ↩︎

  3. HTTPでカプセル化してサーバーに渡し、サーバーでHTTPを外して中身のTCP/UDPを転送するという方法はありはしますが・・・ ↩︎

Discussion