ブラウザで動かすPythonの出来ること出来ないこと
はじめに
最近はPythonがブラウザで動くようになってきています。
この前はscriptタグにPythonを書くPyScriptが話題になっていましたね。
ただ、現状まだ発展途上で出来ることと出来ないことが混在しています。
そこで、2022/06時点での状況をまとめたいと思います
ちなみに、この記事の内容は個人開発でブラウザ上で動くPythonのPlayground
を作っていて得た知見をもとに作成していますブラウザで動く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
の引数に文字列で渡して実行することができます
CodePenのURLが不正です Pythonのコードを実行し、jsで戻り値を取得できます
CodePenのURLが不正です 逆にjsをPython側で実行もできます
import js
でjsのグローバル変数にPython側でアクセスできます。グローバル変数から辿れる関数を実行できます
CodePenのURLが不正です pythonで書いた関数をjs側に渡してjs側で実行
なお、Js <-> Python のやり取りの際に型の変換が行われます。
細かい変換はドキュメントで確認できます
ブラウザ内にファイルシステム
Emscriptenの機能で疑似的なファイルシステムをブラウザ内に作ってくれます。
デフォルトではメモリ上に作られるため揮発的ですが、ブラウザのストレージに保存して永続化もできるようです
pip install
で動的にパッケージインストール
ブラウザ内にブラウザの中にpip installで動的にインストールするというのは個人的には衝撃的でした。
Cで書かれたパッケージは事前コンパイルとPyodideへの登録が必要ですが、
純粋にPythonで書かれたパッケージでwheelのあるものはPyPIから直接インストールすることができます。
PyodideでWASMにコンパイル済みのもの
pyodide.loadPackage
をjs側で実行すればインストールできます
PyPIのパッケージ
micropipというパッケージを使うとPyPIからインストールできます
CodePenのURLが不正ですPyodideの出来ないこと
C/C++で書かれたパッケージの動的インストール
WASMコンパイルしないといけないため、当然できません。
事前にコンパイルしてPyodideに登録する必要があります
関数呼び出し回数上限の変更
WASM側のコールスタックサイズの問題で、Python側の関数呼び出し上限設定を大きくしても小さな呼び出し回数でエラーを起こしてしまいます
スレッド
Emscriptenではサポートされており、将来的に実装される見込みです
同期的なsleep(同期IO)
ブラウザは基本的にシングルスレッドなため、sleepする手段がありません。
そのためtime.sleep
はデフォルトでは何もしない関数になっており、sleepせずに即座に制御を返してきます。
async-awaitを使えば疑似的なsleepはできますが、そのためにはPythonのコードをasync-awaitで書く必要があります。
sleepの実現方法はこれまでいろいろな方法が議論がされていたようです。
このissueには次のことが書かれています
- コードをすべてasync-awaitに書き換えてから実行する
- AsyncifyというEmscripten の提供する機能を使い、WASM実行を非同期実行にする
- 速度低下が著しいため却下された模様
また、ドキュメントにはWeb Worker上でAtomics.wait
とSharedArrayBuffer
を使った実行中断方法が書かれています
冒頭で挙げた私の作ったプレイグラウンドは試しにこの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埋め込みで確認できます。🖊ボタンを押すとリンク先に飛ぶようにしていますが、アクセス拒否画面が出ます
chromeのNetworkタブの警告
ちなみに、sandboxでないiframeなら問題なく飛ぶことができます。はてなブログに直接iframeを書いた例です
(zennに埋め込みたかったですがiframeをサポートしておらず・・・)
ソケット通信
ブラウザの中なので、ソケットを用意してもTCP/UDPの通信を外部へ送るすべがありません[3]。
感想
PyodideはPythonとJsのコードが混ざってカオス
正直Pythonのコードを扱いやすいとは言えない現状です。
インデントはどうもよしなにやってくれるようでそこまで困らなかったのですが、シンタックスハイライト等が効かずなかなか読みづらいです。
また、発生する例外のトレースもJsとPythonが混ざって分かりづらくなってます。
こういうところがPyScriptのモチベーションなんだろうと思います
PyodideはjsでPythonのライブラリを使うためのものと思ってよさそう
機械学習で作ったAIやPythonの豊富なライブラリをブラウザで直接実行できる、というのが一番の用途なのではと感じました。
ちなみに画像処理のコードも載せておきます。
jsだとぱっとググった感じではあまりいい書き方が見つかりませんでしたが、pythonなら数行ですね
画像処理ライブラリPillowの実行例
Play Groundを作るには早すぎたかな・・・
Cを使うライブラリを動的にインストールできない点や、スレッド等が未実装でいろいろ動かせないためです。まあ作ってしまったので公開するのですが
まだまだ発展途上ではありますが、Pythonのライブラリをブラウザで使うにはいいツールだと思いました
Discussion