はじめに
現在、個人開発している「SmartMemo」に、Pythonコードを実行できる機能を追加しています。SmartMemoは、Markdownでメモを書きながら、その中にPythonコードを記述して実行できるアプリです。
これまでは、
print("Hello,World!")
のようなコードを実行し、実行結果をMarkdownの中に自動で記録するところまで実装していました。しかし、Pythonを実行できるようになると、次の欲しくなったのが、
name = input("名前を入力してください:")
print("こんにちは",name)
のようなinput()による入力機能です。今回は、このinput()対応を実装しました。
今回実装したこと
今回の実装では、Pythonの
input()
をSmartMemo上の入力フォームに置き換える仕組みを作りました。
最終的には、
Pythonコード
↓
input()を検出
↓
web Workerから入力要求
↓
SmartMemoに入力欄を表示
↓
ユーザーが入力
↓
Workerへ入力値を送信
↓
Pythonの処理を再開
↓
実行結果をMarkdownへ記録
という流れになっています。
PyodideをWeb Workerで実行
SmartMemoでは、Pythonコードの実行にPyodideを使用しています。また、Pythonの実行によってブラウザの画面が固まらないように、PyodideはWeb Worker上で動かしています。
そのため、Pythonコードは、
pyodide.runPythonAsync(convertedCode);
によってWorker内で実行しています。ここでは問題になったのがinput()でした。
input()をそのまま実行するとエラーになる
最初は普通にPythonの
name = input("名前:")
を実行すればよいと思っていました。
ところが、Web Worker内では通常のブラウザUIを直接操作ができません。
実際に試してみると、
ReferenceError: prompt is not defined
などのエラーが発生しました。さらにPyodide側では、
OSError:[Errno 29]I/O error
のようなエラーも発生しました。
ここで、「Pythonのinput()をそのまま使うのが難しい」ということが分かりました。
setStdin()も試してみた
次にPyodideの標準入力機能であるsetStdin()を使う方法も試しました。
しかし、今回の構成では、
stdin
からWebページ上の入力フォームに繋げる必要があります。ユーザーが入力するまで待つ必要があるため、非同期処理との組み合わせが問題になりました。
setStdin()のコールバックからPromiseを返して待機する方法も試しましたが、上手くいきませんでした。そこで、Pythonの標準入力そのものを使うのではなく、SmartMemo独自の入力処理を作る方向に変更しました。
smart_input()を作る
そこでWorker側に、
async function smartInput(prompt){
self.postMessage({
type: "input_request",
prompt: prompt
});
return new Promise(function(resolve){
inputResolver = resolve;
});
}
という関数を作りました。
この関数が呼ばれると、
Worker
↓
input_request
↓
メインスレッド
という形で入力要求を送ります。
SmartMemo側で入力フォームを表示
input_requestを受け取ったJavaScript側では、入力フォームを動的に生成します。

ユーザーが「入力」を押すと、
pyodideWorker.postMessage({
type:'input',
userInput: userInput
});
として、待機していたPromiseを解決します。これによって、Python側の処理が再開されます。
input()をsmart_input()へ交換
もう一つ問題になったのが、
input("名前:")
をどうやってSmartMemo独自の処理に繋げるかです。そこで、Pythonコードを実行する前にJavaScriptでinput()を検出し、
const convertedCode = code.replace(
/input\(\s*(["'])(.*?)\1\s*\)/g,
'await smart_input("$2")'
);
という形で交換することにしました。例えば、
name = input("名前:")
print("こんにちは", name)
が
name = await smart_input("名前:")
print("こんにちは", name)
という形になります。PyodideのrunPythonAsync()ではトップレベルのawaitを利用できるため、この方法で入力待ちを実現しました。
複数のinput()にも対応
最初は1回の入力だけを考えていましたが、実際には、
name = input("名前:")
age = input("年齢:")
print("名前:", name)
print("年齢:", age)
のようなコードも実行したくなります。そこで、入力が1回終わったら次のinput()へ進む仕組みにしました。
名前:
[入力欄]
↓
年齢:
[入力欄]
↓
実行結果
という流れで複数の入力を処理できるようになりました。
int(input())にも対応
さらに、
age = int(input("年齢:"))
のように、input()で受け取った値をint()で数値に変換するコードにも対応しています。
input()で受け取った入力値は、Pythonでは文字列(str)として扱われます。
そのため、数字として計算などに利用したい場合は、int()を使って整数に変換します。
実際に、
num = int(input("数字を入力してください:"))
print("入力値:", num)
でも正常に動作することを確認しました。
実装中に起きた問題
今回、一番苦労したのが入力UIと実行結果の扱いでした。
最初はPythonの出力を、
outputDiv.textContent += text;
のように親要素へ直接追加していました。しかし、入力フォームも同じoutputDivの中に入っているため、textContentを書き換えると、入力フォームそのものが消えてしまう問題が起きました。問題を解決するため、出力部分を分離しました。
outputDiv
├── outputTextDiv
└── inputDiv
Pythonのprint()は、
[data-smart-output-text]
だけに追加するようにし、これによってPythonの出力と入力UIを分離できました。
Stop→Runで入力欄が二重になる問題
もう一つ問題になったのが、
Run
↓
input()
↓
Stop
↓
Run
という操作で、Run押してからStopを押して、再度Runすると入力欄が2つ表示される、問題がありました。

そこでStopの処理の中で、
const inputDiv =
outputDiv.querySelector('[data-smart-input]');
if(inputDiv){
inputDiv.remove();
}
として、残っている入力UIを削除するようにしました。
これで、
Stop
↓
入力UI削除
↓
Worker再作成
↓
Run
↓
入力UIを1つだけ表示
という動作になりました。
最終的にできるようになったこと
- input()による入力
- 複数のinput()
- int(input())
- print()出力
- Python実行エラー
- エラー後の再実行
- Stop → Run
- 入力UIの重複防止
まとめ
今回のinput()は、SmartMemoではPyodideをWeb Worker上で動かしているため、通常のinput()処理ができないので、PythonからJavaScriptへ入力要求を送り、ブラウザ上の入力フォームでユーザーの入力を受け取り、その値を再びWeb Workerへ渡しPythonの処理を再開する仕組みを実装しました。
処理は以下のものになります。
```text
Python
↓
Pyodide
↓
Web Worker
↓
JavaScript
↓
HTML入力フォーム
↓
Web Worker
↓
Pyodide
↓
Python
おわりに
個人開発では、完璧に作るよりは、動かしてみる。エラーになる。原因を調べる。修正する。動かしてみる。の繰り返しが大事なのだと思いました。今後はさらに、Python実行で、グラフ表示なども挑戦していきたいと思っています。来週からは、SQLiteからPostgreSQLに移行後 → Multi-languageへ進もうと思います。