PyScript⑧(PyScriptからJavaScriptをコール)

PyScriptからJavaScriptをコール

PyScriptからは、JavaScriptの変数を読み込んだり、JavaScriptの関数をコールしたりすることができます。

PyScriptからJavaScriptの変数や関数をインポートするためにはfrom js import ...と宣言します。

単純なJavaScriptの変数は、同等のPythonのデータ型にコンバートされます。(暗黙的コンバート

より複雑なオブジェクトはJSProxyオブジェクトにラップされ、Pythonオブジェクトのように動作します。

[ソースコード]

j2p.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>

<script>
name = "Guido" // JavaScriptの変数
function addTwoNumbers(x, y){ // JavaScriptの関数
return x + y;
}
</script>

<body>
<py-script>
# PythonからJavaScriptの変数と関数をインポートする
from js import name, addTwoNumbers

print(f"Hello {name}")
print("Adding 1 and 2 in Javascript: " + str(addTwoNumbers(1, 2)))
</py-script>
</body>
</html>

[ブラウザ表示]

PyScriptからJavaScriptの変数を参照したり、JavaScriptの関数をコールしたりすることができました。

PyScript⑦(print関数)

print関数

ブラウザ上に一番簡単にデータを表示する方法はprint関数を使うことです。

Python開発者にもっともなじみのあるprint関数PyScriptで使用するとpy-terminal要素にデータを表示することができます。

[ソースコード]

print.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>

<body>
<button py-click="print_to_page()" id="print">Print Things!</button>

<py-script>
def print_to_page():
print("I print things!")
</py-script>

</body>
</html>

[ブラウザ表示]

ボタンを押すと、ブラウザにすでに表示されている要素(今回のサンプルではボタンのみ)の下に黒いキャンバスが現れ、その中にprint関数で指定した文字列が表示されます。

この黒いキャンバスがpy-terminal要素です。

PyScriptでは、デフォルトで標準出力標準エラーがこのpy-terminal要素に表示されます。

またpy-terminalタグをHtml上に明示的に指定することで、特定の位置にpy-terminal要素を表示することもできます。

PyScript⑥(display関数)

display関数

APIのdisplay関数を使うと、簡単にブラウザ上にコンテンツを表示することができます・

このdisplay関数では、文字列だけではなくイメージマークダウンSVGデータjsonも表示することができます。

サンプルコード

APIdisplay関数の第1引数には、ページに表示するデータを設定します。

また必ずtargetパラメータを指定して、どこにコンテンツを表示するかを設定する必要があります。

[ソースコード]

clock.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<title>Writing to the page</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>

<body>
<div id="display-write"></div>
<button py-click="display_to_div()" id="display">Say Things!</button>

<py-script>
def display_to_div():
display("I display things!", target="display-write")
# display("I display things!", target="display-write", append=False) # 追記しない
</py-script>
</body>
</html>

[ブラウザ表示]

ボタンを押すと、display関数で表示したデータが表示されることを確認できます。

ただ、ボタンを押すたびに何度も文字列が表示されてしまうのは困りますが、これはappendパラメータFalseを設定することで対処できます😊

PyScript⑤(クリック)

クリック

ボタンクリックを検知して、文字列を表示するサンプルを作成します。

ボタンクリック時にはPyScript内の、Pythonで定義した関数で処理を行います。

サンプルコード

まず、manual-writeというidのdiv要素を作成します。

次にpy-scriptタグ内に、関数write_to_pageを定義しボタンクリック時の処理としてmanual-write要素のテキストに”Hello World”を設定するコードを書きます。

ボタンにはpy-click属性に上記で定義した関数write_to_page()を設定します。

なおpy-click属性を使う際には、合わせて任意のid属性を設定する必要がありますので注意して下さい。

[ソースコード]

clock.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>

<body>
<div id="manual-write"></div>
<button py-click="write_to_page()" id="manual">Say Hello</button>

<py-script>
def write_to_page():
manual_div = Element("manual-write")
manual_div.element.innerText = "Hello World"
</py-script>
</body>
</html>

[ブラウザ表示]

ボタンをクリックすると、ボタンの上に「Hello World」という文字列が表示されることを確認できました。

PyScript④(時刻を動的に表示)

時刻を動的に表示

PyScriptを使って、時刻を動的に表示してみます。

サンプルコード

動的な表示をするためにはまずimport asyncioと宣言しasyncioライブラリをインポートしておきます。

次に動的に動かしたい関数(今回はfoo関数)にasyncという修飾子をつけます。

foo関数内では、繰り返したい処理をwhile True配下に記載します。

今回は、時刻を取得しそれをoutputDiv2outputDiv3に反映しています。

またawait asyncio.sleep(1)を使って、1秒待ってから実行するようにしています。

[ソースコード]

clock.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>

<body>

<div class="font-mono">start time: <label id="outputDiv"></label></div>
<div id="outputDiv2" class="font-mono"></div>
<div id="outputDiv3" class="font-mono"></div>

<py-script>
import asyncio
from datetime import datetime as dt

def format_date(dt_, fmt="%m/%d/%Y, %H:%M:%S"):
return f"{dt_:{fmt}}"

def now(fmt="%m/%d/%Y, %H:%M:%S"):
return format_date(dt.now(), fmt)

async def foo():
while True:
await asyncio.sleep(1)
output = now()
Element("outputDiv2").write(output)

out3 = Element("outputDiv3")
if output[-1] in ["0", "4", "8"]:
out3.write("It's espresso time!")
else:
out3.clear()

pyscript.run_until_complete(foo())
</py-script>
</body>
</html>

[ブラウザ表示]

ブラウザ上に時刻が1秒ごとに更新される(動的に表示される)ことを確認できました。

PyScript③(イベント)

イベント

PyScriptではイベント処理を行うことができます。

今回は、赤・白・黄を選択するラジオボタンを表示し、どのボタンが選択されているかを確認してみます。

サンプルコード

イベント処理を行うためには、まずfrom pyodide.ffi import create_proxyを宣言します。


次にイベントが発生した際に呼び出される関数(select_color関数)を定義します。

この関数内では、Html上のコンポーネントから情報を読み取り、その情報を表示しています。


Html上のコンポーネントはjs.document.getElementsByName関数を使って取得し、取得した各コンポーネントに対してaddEventListener関数を使ってイベントリスナーを設定します。

このaddEventListener関数の第1引数にはイベントの種類を設定し、第2引数にはcreate_proxy関数で作成したプロキシを設定します。

なおcreate_proxy関数の第1引数には最初に定義したselect_color関数を指定しています。

[ソースコード]

Event.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<html>
<head>
<title>Color Picker</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>

<py-script>
from pyodide.ffi import create_proxy

def select_color(event):
print('--- イベント発生 ---')
for ele in color_elements:
print(ele.id, ele.value, ele.checked)

color_elements = js.document.getElementsByName("color")
ele_proxy = create_proxy(select_color)
for ele in color_elements:
if ele.value == "red":
ele.checked = True
ele.addEventListener("change", ele_proxy)
</py-script>

<div id="input" style="margin: 20px;">
色を選んでください。: <br/>
<input type="radio" id="id1" name="color" value="red">
<label for="all"></label>
<input type="radio" id="id2" name="color" value="white">
<label for="chocolate"></label>
<input type="radio" id="id3" name="color" value="yellow">
<label for="cherrie"></label>
</div>

</body>
</html>

[ブラウザ表示]

選択肢の色をクリックするたびにイベントが発生し、選択された色についてはele.checkedTrueとなっていることが確認できました。

PyScript②(ライブラリ)

ライブラリ

PyScriptでライブラリを使用するときには、py-config タグを使います。

例えば matplotlibpandas を使用するためには、下記のように記述します。

1
2
3
<py-config>
packages = ["matplotlib", "pandas"]
</py-config>

サンプルコード

使用するライブラリをpy-configタグで宣言することにより、py-scriptタグ内でそのライブラリをインポートすることができるようになります。

下記のサンプルコードでは、CSVファイルをダウンロードし、pandasライブラリを使ってそのCSVファイルを読み込み、matplotlibライブラリを使って棒グラフを描画しています。

[ソースコード]

matplotlib.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html>
<head>
<title>Ice Cream Picker</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>

<py-config>
packages = ["matplotlib", "pandas"]
</py-config>

<py-script>
import pandas as pd
import matplotlib.pyplot as plt

from pyodide.http import open_url

url = (
"https://raw.githubusercontent.com/Cheukting/pyscript-ice-cream/main/bj-products.csv"
)
ice_data = pd.read_csv(open_url(url))

def plot(data):
plt.rcParams["figure.figsize"] = (24,12)
fig, ax = plt.subplots()
bars = ax.barh(data["name"], data["rating"], height=0.7)
ax.bar_label(bars)
plt.title("Rating of ice cream flavours of your choice")
display(fig, target="graph-area", append=False)

plot(ice_data)
</py-script>

<div id="graph-area"></div>
</body>
</html>

[ブラウザ表示]

上記のようにブラウザ上に、棒グラフを描画することができました。

PyScript①(Hello, World!を表示)

PyScript

PyScriptを使うと、Htmlの中でPythonのコードを実行することができます。

インストール等は必要ありませんが、HTML読み込み時にランライムのロードやコンポーネントの初期化処理があるため数秒待つ必要があります。

サンプルコード

「Hello, World!」を表示するサンプルを作成します。

PyScriptを実行するためには、HTMLのヘッダー部に次の2行を追加します。

1
2
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>

Pythonコードは、py-scriptタグの間に記載します。(7~9行目)

[ソースコード]

helloworld.html
1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<py-script>
print('Hello, World!')
</py-script>
</body>
</html>

[ブラウザ表示]

ブラウザ上に、Pythonコードのprintで表示した文字列‘Hello, World!’を表示することができました。

重複組み合わせ

問題(重複組み合わせ)

$ n $ 種類の品物があり、 $ i $ 番目の品物は $ a_i $ 個あります。

異なる種類の品物同士は区別できますが、同じ種類の品物同士は区別できません。

これらの品物の中から $ m $ 個選ぶ組み合わせの総数を求めて下さい。

[制約]
🔹$ 1 \leqq n \leqq 1000 $
🔹$ 1 \leqq m \leqq 1000 $
🔹$ 1 \leqq a_i \leqq 1000 $

解き方・ソースコード

重複なく数え上げるために、同じ種類の品物を一度に処理します。

漸化式を次のように定義します。

$$ dp[i+1][j] := i 番目までの品物からj個選ぶ組み合わせの総数 $$

$ i $ 番目までの品物から $ j $ 個選ぶためには、$ i - 1 $ 番目までの品物から $ j - k $ 個選んで、$ i $ 番目の品物を $ k $ 加えればよいので

$$ dp[i+1][j] = \sum_{k=0}^{min(j,a[i])}dp[i][j-k] $$

という漸化式が成り立ちます。

この漸化式は計算量が $ O(nm^2) $ となりますが、

$$ \sum_{k=0}^{min(j,a[i])}dp[i][j-k] = \sum_{k=0}^{min(j-1,a[i])}dp[i][j-1-k]+dp[i][j]-dp[i][j-1-a_i] $$

であるので、

$$ dp[i+1][j]=dp[i+1][j-1] + dp[i][j]-dp[i][j-1-a_i] $$

と変形することができ、計算量は$ O(nm) $ となります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#--------- 入力例 ----------
n = 3 # 品物の数
a = [1, 2, 3] # 各品物の個数
m = 3 # 分割数
#---------------------------
memo = {}

# 1つも選ばない方法は常に一通り
for i in range(n + 1):
memo[i, 0] = 1

for i in range(n): # 品物の数に対応するループ
for j in range(1, m + 1): # 分割数に対応するループ
if j - i -a[i] >= 0:
memo[i + 1, j] = memo.get((i + 1, j - 1), 0) + memo.get((i, j), 0) - memo.get((i, j - 1 - a[i]), 0)
else:
memo[i + 1, j] = memo.get((i + 1, j - 1), 0) + memo.get((i, j), 0)

print('解:', memo[m, n])

[実行結果]

解: 6

[1 2 0]、[1 1 1]、[1 0 2]、[0 2 1]、[0 1 2]、[0 0 3]の6パターンあるので、解は6となります。

分割数

問題(分割数)

$ n $ 個の品物を、$ m $ 個以下に分割する方法の総数を求めて下さい。

解き方・ソースコード

n の m 分割 $ a_i ( \sum_{i=1}^{m}a_i=n ) $ を考えてみます。

全ての $ i $ で $ a_i > 0 $ ならば、$ {a_i - 1} $ は $ n - m $ の $ m $ 分割となります。

また、$ a_i = 0 $ となる $ i $ が存在したら、これは $ n $ の $ m - 1 $ 分割となります。

したがって、次のような漸化式を立てることができます。

$$ dp[i][j] = dp[i][j - i] + dp[i - 1][j] $$

この漸化式ならば重複なく分割数を数え上げることができ、計算量は $ 0(mn) $ となります。

[Google Colaboratory]

1
2
3
4
5
6
7
8
9
10
11
12
13
#--------- 入力例 -----------
n = 4 # 品物の数
m = 3 # 分割数
#----------------------------
memo = {(0,0): 1}

for i in range(1, m + 1): # 分割数に対応するループ
for j in range(n + 1): # 品物の数に対応するループ
if j - i >= 0:
memo[i, j] = memo.get((i - 1, j), 0) + memo.get((i, j - i), 0)
else:
memo[i, j] = memo[i - 1, j]
print('解:', memo[m, n])

[実行結果]

解: 4

4個の品物を3分割以下にする方法は、[1 1 2]、[1 3]、[2 2]、[4]の4パターンあるので、解は4となります。