ロゴ ロゴ

pyodideはまあまあ使える

まえがき

新歓真っ盛りなこの頃,皆さんも入りたいサークルを探している最中でしょうか.自分に合ったサークルが見つかることをお祈りしております.どうもウグイスです.

新歓期間中なのでブログ多めでやろうと思います.

さて今回はフロントエンドの技術でちょっと面白いなと思ったものがあったのでちょっと紹介させてもらいます.

動機

実は研究の一環で生体情報処理系のWebアプリを作成していたのですが,大きな壁に当たりました.フロントエンドではPythonが動かないのです.

何が困るって,生データから前処理したいとか,指標の算出をしたいとかあるんですが,コード資産の問題でPythonならコードがあるけど,JSだと自前で実装しないといけないという状態です.(自前実装はだるすぎる)

Pythonでできるならバックエンドサーバを使ってやればいいじゃんと思うかもしれないですが,正直計算資源を提供するのはちょっと嫌というか,大した処理でもないので,フロントでできるならそっちでやってほしい感がある.

というわけで,どうしようかなと悩んでいたところ,pyodideでやればいいんじゃね?という意見をもらったので,試しにやってみたというわけです.

入門

とりあえず試したいので,公式のUsingページを参考に,ローカルで立てて実験してみます.

<!doctype html>
<html>
  <head>
      <script src="https://cdn.jsdelivr.net/pyodide/v0.27.4/full/pyodide.js"></script>
  </head>
  <body>
    <script type="text/javascript">
      async function main(){
        let pyodide = await loadPyodide();
        console.log(pyodide.runPython("1 + 2"));
      }
      main();
    </script>
  </body>
</html>

こいつを適当にローカルに持ってきて保存して,pythonのhttp.serverを使ってみてみましょう.

立ち上げたら,開発者モードを起動してみてください.以下のように出力されると思います.

動いていることは確認できました.

で私はsvelte kitでフロントを作っているので,そちらに合わせて,使えるようにしてみようかと思います.

svelte kitで使う

というわけでプロジェクトをセットアップ

npx sv create
npm install

npmを使うので,npm i pyodideとかで使えるようになるんですが,SSGを通すためと,ライブラリを自前で配信したくないので,cdnから持ってくるようにスクリプトを組みます.

pyodideは結構ロードに時間がかかるので,再利用可能にするためにwindowオブジェクトに突っ込んでます.

共通ライブラリかつ,フロントで動いてほしいので,src/lib/以下にソースを置きます.

src/lib/loadPyodide.js

export async function loadPyodideInstance() {
    if (window.pyodide) {
        return window.pyodide;
    }

    const pyodideScript = document.createElement("script");
    pyodideScript.src = "https://cdn.jsdelivr.net/pyodide/v0.27.3/full/pyodide.js";
    pyodideScript.async = true;

    document.head.appendChild(pyodideScript);

    await new Promise((resolve) => {
        pyodideScript.onload = resolve;
    });

    window.pyodide = await loadPyodide({
        indexURL: "https://cdn.jsdelivr.net/pyodide/v0.27.3/full/",
    });

    console.log("Pyodide loaded successfully!");
    return window.pyodide;
}

複数のアプリを作ってみるということで,トップページにリンクを置いて,各ページに飛べるようにします.

/src/routes/+page.svelte

<script>
    const pages = [
        {
            link: "/calc_add",
            text: "Pythonで足し算"
        }
    ]
</script>

<h1>Pyodideで遊んでみた</h1>

<h2>作ったもの一覧</h2>

{#each pages as page}
<a href={page.link}>{page.text}</a>
{/each}

足し算アプリ

まずは,簡単に実装できるので足し算アプリを作ってみます.

2つの入力ボックスに数字を入力したら,計算する感じで行きます.derived.byを使って状態が変わったら即時に反映されるようにします.

/src/routes/calc_add/+page.svelte

<script>
    import { loadPyodideInstance } from "lib/loadPyodide";
    import { onMount } from "svelte";

    let pyodide

    let in1 =state(0)
    let in2 = state(0)
    let res =derived.by(async() => {
        return calcAdd(in1, in2)
    })

    onMount(async() => {
        pyodide = await loadPyodideInstance()
    })

    async function calcAdd(in1, in2) {
        pyodide = await loadPyodideInstance()
        let context = pyodide.toPy({in1, in2})
        const result = pyodide.runPython("in1 + in2", { globals: context })
        return result
    }
</script>

<h1>Pythonで足し算</h1>

<div>
    <input type="number" bind:value={in1}/> + <input type="number" bind:value={in2}/> = {#await res} caluculating... {:then result} {result} {:catch name} Error... {name} {/await}
</div>

onMountを使って,pyodideを初手でロードさせます.そのうえで,計算結果が来れば,それを表示するようにしました.

まあこんな感じです.一応エラー対応もしましたが,そこまで敏感に描かなくてもいいのかなと思います.

ユーザが入力したコードを実行する

次は,ユーザが入力したコードを実行してみようという感じで行きます.

エディタはmonaco editorを使いました.とりあえずソース

/src/route/user_script_exe/+page.svelte

<script>
    import { loadPyodideInstance } from "lib/loadPyodide";
    import { onMount } from "svelte";

    let errList =state([])
    let outList = $state([])
    let editordiv
    let pyEditor

    onMount(async() => {
        const { editor, KeyCode, KeyMod } = await import('monaco-editor/esm/vs/editor/editor.main.js')
        pyodide = await loadPyodideInstance()
        pyodide.setStdout({batched: (str) => outList.push(str)})
        pyodide.setStderr({batched: (str) => outList.push(str)})
        pyEditor = editor.create(editordiv, {
            value: "",
            language: "python",
            automaticLayout: true,
            theme: "vs-dark"
        })
        pyEditor.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, exeScript)
    })

    async function exeScript() {
        const userPyScript = pyEditor.getValue()
        pyodide = await loadPyodideInstance()
        try {
            pyodide.runPython(userPyScript)
        }
        catch(e) {
            errList.push(e)
        }
    }
</script>

<h1>ユーザスクリプト実行</h1>

<h2>Pythonスクリプトエディタ</h2>

<div class="editor" bind:this={editordiv}></div>

<button onclick={exeScript}>Run! (ctrl + Enter)</button>

<h2>アウトプット</h2>

<div class="result">
    <section>
        <h3>stdout</h3>
        {#each outList as out}
            {out}
            <br>
        {/each}
    </section>
    <section>
        <h3>Error Message</h3>
        {#each errList as err}
           <p class="errMsg">{err}</p>
        {/each}
    </section>
</div>

<style>
    section {
        width: 50%;
        box-sizing: border-box;
        border: 1px solid #ddd;
    }

    .editor {
        width: auto;
        height: 50vh;
    }

    .result {
        display: flex;
        justify-content: flex-start;
    }

    button {
        font-size: large;
        background-color: rgb(119, 255, 119);
        border-color: none;
    }
</style>

Styleが一部ブログの都合上載せられなかったので,完全なコードはGithubリポジトリコードを参照してもらいたい.

実行するとこんな感じ

実際は無限ループとかに備えて,非同期で実行して,中断できるように作るべきなのですが,今回は簡易実装ということで

一応工夫というか,monaco-editorはブラウザ側でしか動かないのでonMount内で動的ロードの形で読み込むようにしました.

ちなみに,これだとimport numpyとかできないので,そのうち修正版を上げれたらいいなと思います.一応,micropipを使えば動的に追加できそうなので,それを使用する予定です.

あとがき

というわけで,pyodideを使って,単純な足し算を行うアプリと,ユーザの入力したスクリプトを実行するアプリを作ってみました.

ちなみにリポジトリも作っておきました.以下のリンクです.

pyodide_test リポジトリ

ついでに,デプロイ機能も付けて公開できるようにしたので,いつでも遊べます.そのリンクも載せときますね.ちょろちょろっと設定ファイルを追加すれば,簡単にデプロイできるGithub.ioは素晴らしい.

https://uguisu64.github.io/pyodide_test/

追加パッケージインストール対応とかいろいろ考えているので,暇な時にでも更新していこうと思ってます.


というわけで,本編は終了です.

ちょっと身内話ですが,当サークルにも少しずつ新入生が入ってくれています.今年は新入生ブログリレーをやるかはわかりませんが,そのうち新入生の初々しいブログが投稿されると思うので,その時はぜひ見てください.

では今回のブログはこれで終わりです.ご精読ありがとうございました.ウグイスでした.

コメント入力

関連サイト