Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
PyO3 で既存 Python 評価器を Rust core 化する ー wasm-bindg...
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
𝕂'
August 21, 2026
Programming
82
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
PyO3 で既存 Python 評価器を Rust core 化する ー wasm-bindgen でブラウザにも配るための設計
PyCon JP 2026.8.21(DAY1) 16:15 – 16:45
https://2026.pycon.jp/ja/talks/FXFNLY
𝕂'
August 21, 2026
More Decks by 𝕂'
See All by 𝕂'
Streamlit は社内ツールだけじゃない!PoC の速さで実現する'商用品質'の分析 SaaS アーキテクチャ
kdash
4
2.8k
Other Decks in Programming
See All in Programming
仕様書を書く前にハーネスを作る - Agent Native開発は「探索を速く、判定を固く」
gotalab555
4
1.8k
わからない話を追いかけたら、プログラミング言語を作る側にいた
ydah
3
550
生成AIで帳票OCRが「簡単に」作れる時代になった?
kon_shou
0
1.2k
プロポーザルを書いてもらう
pvcresin
0
580
Webエンジニアなのにブラウザの仕組みがわからないので、Pythonで自作してみた
tatsuki12
4
1.1k
My Marp Sample
sinoue0108
0
120
進化を続けるGo toolsの現在地 / The Current State of Ever-Evolving Go Tools
hond0413
0
260
まずはプロンプトガイドを読もう、話はそれからだ
kiakiraki
1
210
S3 を使うアプリケーションをローカル完結で動かすことに全力を注いでみた / Running S3 Apps Offline
contour_gara
0
620
React本体のコードリーディング
high_g_engineer
1
160
引き算の組織 ― アウトカムとAIに全振りするために辞めたこと ― / Organization by Subtraction
hirokiyamamoto14
PRO
0
300
不幸な GC
chencmd
0
740
Featured
See All Featured
Abbi's Birthday
coloredviolet
3
9.5k
Chasing Engaging Ingredients in Design
codingconduct
0
280
Build your cross-platform service in a week with App Engine
jlugia
234
19k
Digital Ethics as a Driver of Design Innovation
axbom
PRO
1
380
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
500
Designing Dashboards & Data Visualisations in Web Apps
destraynor
232
55k
Heart Work Chapter 1 - Part 1
lfama
PRO
8
36k
Design of three-dimensional binary manipulators for pick-and-place task avoiding obstacles (IECON2024)
konakalab
0
550
No one is an island. Learnings from fostering a developers community.
thoeni
21
3.8k
Organizational Design Perspectives: An Ontology of Organizational Design Elements
kimpetersen
PRO
1
800
The Anti-SEO Checklist Checklist. Pubcon Cyber Week
ryanjones
0
220
ラッコキーワード サービス紹介資料
rakko
1
4.5M
Transcript
PyO3 で既存 Python 評価器を Rust core 化する wasm-bindgen でブラウザにも配るための設計 PyCon
JP 2026.8.21(DAY1) Recustomer 株式会社 古川 祐希 16:15 – 16:45
自己紹介 古川 祐希 K-dash 所属: Recustomer 株式会社 Python 歴: 約5年
Rust 歴: 約1年 略歴 2012 - 2018 SIer でインフラエンジニア 2018 - 2025 Webアプリケーション開発 サーバー/ネットワーク構築自動化 2025/04 - Recustomer の Platform Team 所属 2025 年の主な活動 PyCon JP 2025 登壇: Streamlit は社内ツールだけじゃない! Qiita Advent Calendar 2025: Python の型ヒントと共に進化するコード
発表内容のスコープ お話すること ✅ お話しないこと ❌ Python のロジックを Rust に 1
本化して配る話 • • • • Rust 言語そのものの入門・文法解説 • PyO3 の全機能の網羅 • 高速化・ベンチマークの話 なぜ、書き直しではなく Rust core 化なのか 題材: Python 評価器の紹介 Rust core 化から、Python へ配るまでの過程を紹介 → 主目的は速度ではなく実装の一本化 ◦ • 注意ポイント 5 つ どんなときに Rust core 化に踏み込む価値があるか
Python のロジックを別の言語で動かしたい! ...というシチュエーションに遭遇したことはありませんか?
一例をあげると、こんなとき Python 製バックエンドのロジックを JavaScript 製フロントエンドでも動かしたい • Why: ユーザー操作中に処理結果を即座に返したい ▪ UI
の入力中にバリデーション結果を出す ▪ 金額の計算結果を出す ▪ ボタンの有効 / 無効を切り替える …
JavaScript で同じロジックを作ればいいじゃない バックエンド フロントエンド 複雑な判定・計算ロジック 複雑な判定・計算ロジック (移植版) バックエンドの Python ロジックを
JavaScript に移植すればブラウザで動く
ただし、2 つのロジックで答えがズレると困る このシチュエーションにおける前提 • フロントエンドは、あくまでも先読み(UX の向上) • バックエンドが最終判定 → 永続化
◦ 移植元と移植先のロジックは完全に同一の挙動でないといけない
ただし、2 つのロジックで答えがズレると困る このシチュエーションにおける前提 • フロントエンドは、あくまでも先読み(UX の向上) • バックエンドが最終判定 → 永続化
◦ 移植元と移植先のロジックは完全に同一の挙動でないといけない この前提を置くと、 単純に移植する手法にはいくつかの落とし穴があります →
移植の落とし穴①: 同じつもりのコードが違う答えを返す 言語仕様の違い(一例なので、これだけではない) 挙動が違う。答えがひっそりと変わるだけ
移植の落とし穴②: 最初は同じでも別々に育って食い違う 運用のズレ • 修正が片方にだけ入り、もう片方に反映されない • 知らぬ間に、それぞれの言語の暗黙の挙動が実装に入り込む ◦ その結果、2つの仕様は少しずつズレ始める 実装が
2 つに分かれた時点から、同じ答えを返し続ける責任が生まれる
落とし穴の原因の正体 落とし穴①も②も、原因は同じロジックが 2 つあること
落とし穴の原因の正体 落とし穴①も②も、原因は同じロジックが 2 つあること 実装が 1 つならズレる余地がそもそも無い
1つのロジックを正本にして両方に配れたら良さそうでは? 正本ロジック バックエンド フロントエンド
この課題を解決するいい案があります
これです core ロジック 一度だけ書いて 1 本化 PyO3 wasm-bindgen バックエンド フロントエンド
core ロジックを Rust にして両方に配る • Rust には、1 つの実装を他の実行環境へ持ち出す 橋 が揃っている
PyO3 Rust の実装を Python から import できるようにする wasm-bindgen Rust の実装をブラウザ (Wasm) で動かせるようにする 書き直して 2 つ持つのではなく、1 つを両側へ配ることができる
core ロジックを任せる言語としても Rust は堅い 例えば、Rust には暗黙の型変換が無い。1 + "2" を実行すると… null
が無い(Option で明示)。例外も無い(Result で明示) 「知らぬ間に暗黙の挙動が入り込む」が構造的に起きにくい
余談:実はもう、みなさんの環境で Rust が動いています この中で、 中身が Rust のものは?
余談:実はもう、みなさんの環境で Rust が動いています この中で、 中身が Rust のものは? 全部です ※ pydantic
は v2 のコア (pydantic-core) が Rust 製
ここからは、実際にやった話をします まずは前提となる題材の説明
弊社 Recustomer について • EC 事業者向けに、 購入〜購入後の体験(配送追跡・返品・交換・キャンセル)を向上させる プラットフォームを提供 プラットフォームを提供 バックエンド
EC 事業者 ストアで購入 返品・配送追跡などの 画面を提供 買い物客 今日の話は、この Python バックエンドの中で始まります
Recustomer 内にクーポンの発行判定処理がある • 購入後のリピート促進として、条件を満たした注文にクーポンを発行する クーポンの発行の条件は、EC 事業者ごとに設定できる
EC 事業者ごとに違うのは、値ではなく条件の形 EC 事業者A の条件 注文金額が 5,000 円以上 なら発行する 違うのは
5,000 という数値だけ。この場合、事業者ごとの設定値 1 個で足りる
EC 事業者ごとに違うのは、値ではなく条件の形 EC 事業者A の条件 注文金額が 5,000 円以上 なら発行する 違うのは
5,000 という数値だけ。この場合、事業者ごとの設定値 1 個で足りる EC事業者 B の条件 注文金額が 10,000 円以上 かつ 「予約」タグがない なら発行する 条件が 2 つになり、AND で繋がった。タグの名前も EC 事業者が自由に決める 設定値をいくつ足しても、条件の形は持てない
愚直に if 文で書くと EC 事業者の増加に伴い条件も増える EC 事業者ごとの条件を if 文で書いた場合のコード例 def
should_issue_coupon(order, tenant_id: str) -> bool: # tenant = EC 事業者 if tenant_id == "A": return order.total >= 5000 if tenant_id == "B": return (order.total >= 10000 and not any(t == "予約" for t in order.tags)) # EC 事業者が増えるたびに条件分岐が増えていく return False
愚直に if 文で書くと EC 事業者の増加に伴い条件も増える EC 事業者ごとの条件を if 文で書いた場合のコード例 def
should_issue_coupon(order, tenant_id: str) -> bool: # tenant = EC 事業者 if tenant_id == "A": return order.total >= 5000 if tenant_id == "B": return (order.total >= 10000 and not any(t == "予約" for t in order.tags)) # EC 事業者が増えるたびに条件分岐が増えていく return False • 条件が変わるたびに、コードを直してデプロイ ◦ • → EC 事業者は自分で条件を変えられず、こちらのリリース待ち 「予約」という EC 事業者の設定が、コードに埋まる ◦ どの事業者がどんな条件かはコードを読まないと分からない。A を直して B を壊すリスク
条件を expression tree(式木)として持つ 条件を if 文ではなく JSON データで持ち、評価する仕組みを 1 つだけ書く
EC 事業者A 注文金額が 5,000 円以上なら出す EC事業者 B 10,000 円以上 かつ 「予約」タグが無い {"version": 1, "params": ["amount"], "body": {"if": { "cond": {"apply": {"function": "ge", "args": [{"var": {"name": "amount"}}, {"const": {"value": 5000}}]}}, "then": {"const": {"value": true}}, "else": {"const": {"value": false}}}}} {"version": 1, "params": ["amount", "tags"], "body": {"if": { "cond": {"apply": {"function": "and", "args": [ {"apply": {"function": "ge", "args": [{"var": {"name": "amount"}}, {"const": {"value": 10000}}]}}, {"apply": {"function": "not_has_tag", "args": [{"var": {"name": "tags"}}, {"const": {"value": "予約"}}]}}]}}, "then": {"const": {"value": true}}, "else": {"const": {"value": false}}}}} EC 事業者が増えても変わるのは式木データ(JSON)だけ
式木の読み方 {"version": 1, "params": ["amount"], "body": {"if": { "cond": {"apply":
{"function": "ge", "args": [{"var": {"name": "amount"}}, {"const": {"value": 5000}}]}}, "then": {"const": {"value": true}}, "else": {"const": {"value": false}}}}} ※ EC 事業者 B の「かつ (AND)」も、同じ形の入れ子で書ける ノードの組み合わせで、どんな条件の形も表現できる
ある日、この判定を Recustomer の外でも動かしたくなった • 今の判定は Recustomer の Python バックエンドの中で完結している ◦
発行するかどうかを返すだけ(即時性は求められない処理)
ある日、この判定を Recustomer の外でも動かしたくなった • 今の判定は Recustomer の Python バックエンドの中で完結している ◦
• 発行するかどうかを返すだけ(即時性は求められない処理) EC 事業者のサイトは、外部の EC サイトプラットフォームの上で動いている ◦ そのカート画面に「クーポンがもらえるか」をその場で表示したい ◦ カートの中身が変わるたびに再判定 → 買い物客のブラウザの中で動かしたい
つまり、以下のようなことをしたい Recustomer 内 Recustomer の外 EC 事業者ごとの条件(式木) + 注文データ 評価ロジック
Recustomer のバックエンドと 同じ評価ロジックを動かしたい EC 事業者が利用する EC サイトプラットフォーム ブラウザ 評価ロジック
JavaScript で書くとロジックが 2 つになってズレ始める Recustomer 内 Recustomer の外 EC 事業者ごとの条件(式木)
+ 注文データ 評価ロジック Recustomer のバックエンドと 同じ評価ロジックを動かしたい EC 事業者が利用する EC サイトプラットフォーム ブラウザ 評価ロジック 同じ 評価するコードが 2つになる
移植の落とし穴(再掲)
移植の落とし穴(再掲) 導入で紹介した移植の落とし穴①②が、 そのまま自分たちの話になった →だから Rust core 化を決めました
前置きが長くなりましたが、本日のお話 Python の評価ロジックを Rust に1本化して(=Rust core 化して)両側へ配る core ロジック 一度だけ書いて一本化
PyO3 Python サーバ 以降、こちらを 「Rust core」と呼びます wasm-bindgen ブラウザ
最終形がこちら 最終形のディレクトリ構成 evaluator/ ├─ core/ 評価ロジック(純 Rust) ├─ bindings-py/ Python
への橋(PyO3) └─ bindings-wasm/ ブラウザへの橋(wasm-bindgen) 以降、ステップごとに作っていきます
STEP 1 評価ロジックを Rust core として定義する evaluator/ ├─ core/ │
├─ Cargo.toml │ └─ src/lib.rs ├─ bindings-py/ └─ bindings-wasm/ 判定ロジック(純 Rust) ←まずはここを作る
式木のノード 4 種は Rust の enum で 4 つの枝にする Python(既存の式木ノード定義)
from dataclasses import dataclass @dataclass(frozen=True) class If: cond: Expr then: Expr els: Expr @dataclass(frozen=True) class Apply: function: str args: list[Expr] @dataclass(frozen=True) class Var: name: str @dataclass(frozen=True) class Const: value: int | float | str | bool Expr = If | Apply | Var | Const
式木のノード 4 種は Rust の enum で 4 つの枝にする Python(既存の式木ノード定義)
from dataclasses import dataclass @dataclass(frozen=True) class If: cond: Expr then: Expr els: Expr core/src/lib.rs(ノードを enum で定義する) @dataclass(frozen=True) class Apply: function: str args: list[Expr] @dataclass(frozen=True) class Var: name: str @dataclass(frozen=True) class Const: value: int | float | str | bool Expr = If | Apply | Var | Const 書き直す enum Expr { If { cond: Box<Expr>, then: Box<Expr>, els: Box<Expr> }, Apply { function: String, args: Vec<Expr> }, Var { name: String }, Const { value: Value }, }
評価ロジックの本体も定義する これも Python から移植(詳細は割愛) core/src/lib.rs (評価器は以下の再帰関数 1 本) pub fn
evaluate(expr: &Expr, params: &Params) -> Result<Value, EvalError> { match expr { Expr::Const { value } => Ok(value.clone()), } } Expr::Var { name } => params.get(name), // 注文データから値を引く Expr::Apply { .. } => { /* 関数を適用して再帰 */ } Expr::If { .. } => { /* cond を評価して分岐 */ }
評価ロジックの本体も定義する これも Python から移植(詳細は割愛) core/src/lib.rs (評価器は以下の再帰関数 1 本) pub fn
evaluate(expr: &Expr, params: &Params) -> Result<Value, EvalError> { match expr { Expr::Const { value } => Ok(value.clone()), Expr::Var { name } => params.get(name), // 注文データから値を引く Expr::Apply { .. } => { /* 関数を適用して再帰 */ } Expr::If { .. } => { /* cond を評価して分岐 */ } } } これで Rust core が完成 この core 関数が判定を実行する唯一の正本になる →実装が 1 つならズレる余地がそもそも無い
STEP 2 Rust core ロジックを Python から呼ぶ evaluator/ ├─ core/
✅ ├─ bindings-py/ │ ├─ Cargo.toml │ └─ src/lib.rs └─ bindings-wasm/ Python への橋(PyO3) ←次はここを作る
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b }
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x)
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) ② add(2, 3) を呼ぶ
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) ② add(2, 3) を呼ぶ ③ add が実行されて 5 が返る
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) ② add(2, 3) を呼ぶ ③ add が実行されて 5 が返る Python 側から見れば 普通の module と関数
PyO3: 書いた Rust を Python から import して利用できる Rust core
calculate_py という名前で ① PyO3 で公開 fn add(a: i64, b: i64) -> i64 { a + b } Python (利用する側) import calculate_py x = calculate_py.add(2, 3) print(x) この矢印の正体 = 橋 次はこれを書きます ② add(2, 3) を呼ぶ ③ add が実行されて 5 が返る Python 側から見れば 普通の module と関数
Rust core ロジックを Python から呼べるようにする ⚠ bindings-py/src/lib.rs(このファイルは core ではありません! bindings-py
層に新しく書く関数です) use pyo3::prelude::*; #[pyfunction] #[pyfunction] という属性マクロを付けると Python の関数として利用できる fn evaluate(tree_json: &str, params: Params) -> PyResult<bool> { }
Rust core ロジックを Python から呼べるようにする ⚠ bindings-py/src/lib.rs(このファイルは core ではありません! bindings-py
層に新しく書く関数です) use pyo3::prelude::*; #[pyfunction] #[pyfunction] という属性マクロを付けると Python の関数として利用できる fn evaluate(tree_json: &str, params: Params) -> PyResult<bool> { // ここで、tree_ json をパースし、core の evaluate を呼んで返す(薄いラッパー) }
Rust core ロジックを Python から呼べるようにする ⚠ bindings-py/src/lib.rs(このファイルは core ではありません! bindings-py
層に新しく書く関数です) use pyo3::prelude::*; #[pyfunction] #[pyfunction] という属性マクロを付けると Python の関数として利用できる fn evaluate(tree_json: &str, params: Params) -> PyResult<bool> { // ここで、tree_ json をパースし、core の evaluate を呼んで返す(薄いラッパー) } ただし、これだけではまだ Python から import できない
関数は、モジュールに載せて初めて import できる • • Python の import が探すのは、関数ではなくモジュール #[pyfunction]
を付けただけでは、まだ Python から見えない bindings-py/src/lib.rs(続き) #[pyfunction] Python 側(これで import できる) import evaluator fn evaluate() {...} evaluator.evaluate( #[pymodule] tree_json, fn evaluator(m: &Bound<'_, PyModule>) -> PyResult<()> { {"amount": 7000, "tags": []}, m.add_function(wrap_pyfunction!(evaluate, m)?) } ) # => True
関数は、モジュールに載せて初めて import できる • • Python の import が探すのは、関数ではなくモジュール #[pyfunction]
を付けただけでは、まだ Python から見えない bindings-py/src/lib.rs(続き) Python 側(これで import できる) import evaluator #[pyfunction] fn evaluate() {...} #[pymodule] evaluator.evaluate( モジュールとして公開する属性マクロ fn evaluator(m: &Bound<'_, PyModule>) -> PyResult<()> { tree_json, {"amount": 7000, "tags": []}, 呼べる m.add_function(wrap_pyfunction!(evaluate, m)?) ) # => True } fn の名前が、そのまま import の名前になる(fn evaluator → import evaluator) これで橋は開通
注意ポイント ① Rust core に属性マクロを持ち込まない 「wrapper は用意せず、core の evaluate に直接属性マクロを付ければいいのでは?」
注意ポイント ① Rust core に属性マクロを持ち込まない 「wrapper は用意せず、core の evaluate に直接属性マクロを付ければいいのでは?」
❌ core に付ける ⭕ 橋にだけ付ける // core/src/lib.rs // bindings-py/src/lib.rs #[pyfunction] // ← core に Python の都合が混入 #[pyfunction] // ← OK! fn evaluate() {...} fn evaluate() {...} • core に属性マクロを付けると、core が PyO3 依存になってしまう ◦ • ブラウザへ配るときに全部剥がして回ることになる core に片側の都合を混ぜないのが鉄則
STEP 3 言語の壁を越える「変換層」を用意する evaluator/ ├─ core/ ✅ ├─ bindings-py/ │
├─ Cargo.toml │ └─ src/lib.rs └─ bindings-wasm/ Python への橋(PyO3) ←引き続き、この中の話
引数と返り値は、言語の境界を往復する Rust 側(core) Python 側 import evaluator 引数: Python の値
→ Rust の値 fn evaluate( evaluator.evaluate( expr: &Expr, tree_json, params: &Params, {"amount": 7000, "tags": []}, ) 返り値: Rust の値 → Python の値 # => True この変換は誰がやる? ) -> Result<Value, EvalError>
引数と返り値は、言語の境界を往復する Rust 側(core) Python 側 引数: Python の値 → Rust
の値 import evaluator fn evaluate( evaluator.evaluate( expr: &Expr, tree_json, params: &Params, {"amount": 7000, "tags": []}, ) 返り値: Rust の値 → Python の値 ) -> Result<Value, EvalError> # => True この変換は誰がやる? 基本的には PyO3 がやってくれます
PyO3 は Python の型と Rust の型の対応表を持っている
型が決まっていれば自動変換してくれる Rust 側 Python 側 引数: Python の値 → Rust
の値 import calculate_py fn add(a: i64, b: i64) -> i64 { a + b x = calculate_py.add(2, 3) print(x) PyO3 が i64 に自動変換 返り値: Rust の値 → Python の値 } この場合、変換コードは 1 行も書かない
一方、evaluate() の params は? Rust 側(core) Python 側 引数: Python
の値 → Rust の値 import evaluator fn evaluate( evaluator.evaluate(tree_json, { "amount": 7000, # int "tags": ["予約"], # list "first_purchase": True, # bool }) expr: &Expr, params: &Params, ) -> Result<Value, EvalError> 返り値: Rust の値 → Python の値 注文データ(int / list / bool が混ざる dict) この Params の型をどうするか?
固定の struct にはしたくない Rust 側(core) struct Params { amount: i64,
tags: Vec<String>, first_purchase: bool, // 項目が増えるたびに、ここを足すことに }
固定の struct にはしたくない Rust 側(core) struct Params { amount: i64,
tags: Vec<String>, first_purchase: bool, // 項目が増えるたびに、ここを足すことに } • • 注文データのどの項目を、どんな型で使うかは、式木が決める ◦ EC 事業者 A の式木 → amount だけ ◦ EC 事業者 B の式木 → amount と tags ◦ EC 事業者 C の式木 → struct に現状ない customer_rank 項目が増えた場合、評価器のコードも都度足していく必要がある 「変わるのは式木データだけ」が崩れてしまう
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; 実体 // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } // Value(型を実行時に持つ enum) enum Value { Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } // Value(型を実行時に持つ enum) enum Value { ④ 7000 は Value::Int(7000) になる Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // 最終的な Params の値 "amount": Int(7000) "tags": List([Str("予約")]) "first_purchase": Bool(true) ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value に変換する関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } ⑤ HashMap に入る // Value(型を実行時に持つ enum) enum Value { ④ 7000 は Value::Int(7000) になる Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
変換層を自作して橋渡しする(変換関数と Value enum) # Python 側の注文データ { "amount": 7000, #
int "tags": ["予約"], # list "first_purchase": True, # bool } ① PyO3 が dict を自動変換 dict → HashMap / キー → String Rust 側(wrapper が受け取る) type Params = HashMap<String, Value>; // 最終的な Params の値 "amount": Int(7000) "tags": List([Str("予約")]) "first_purchase": Bool(true) 変換層として自作するのは、この2つ ② amount の値 7000 を Value にしたい → PyO3 が自作の to_value(7000) を呼んでくれる // ③ Python の値 1 個を Value にする関数を実行 fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool?→ int?→ float? … の順に判定して Value に詰める } // PyO3 に「Value はこれで変換」と登録 impl FromPyObject<'_> for Value { fn extract_bound(obj: &Bound<PyAny>) -> PyResult<Self> { to_value(obj) } } ⑤ HashMap に入る // Value(型を実行時に持つ enum) enum Value { ④ 7000 は Value::Int(7000) になる Int(i64), Float(f64), Bool(bool), Str(String), List(Vec<Value>), } // 中身の決め方は後述
注意ポイント ② Value の型は、Python にも JavaScript にも寄せない 前ページの enum Value、中身の対応付けは以下のように決めた
注意ポイント ② Value の型は、Python にも JavaScript にも寄せない 前ページの enum Value、中身の対応付けは以下のように決めた
• int と float は ひとくくりにしない ◦ • JS の number(区別がない)に寄せない bool は int の サブタイプにしない ◦ Python の bool ⊂ int を持ち込まない どちらの言語にも寄せず、最大公約数で決める
注意ポイント ③ 変換関数では、bool を int より先に判定する • Python の bool
は int の サブタイプ ◦ → isinstance(True, int) は True になる ❌ 数値から先に判定すると... fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // float? str? list? … と続く }
注意ポイント ③ 変換関数では、bool を int より先に判定する • Python の bool
は int の サブタイプ ◦ → isinstance(True, int) は True になる ❌ 数値から先に判定すると... fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // float? str? list? … と続く } 先に int を見ると、True が Int(1) になって事故る
注意ポイント ③ 変換関数では、bool を int より先に判定する • Python の bool
は int の サブタイプ ◦ → isinstance(True, int) は True になる ❌ 数値から先に判定すると... ⭕ bool を先に見る fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // float? str? list? … と続く } fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // bool 判定 if let Ok(b) = obj.downcast::<PyBool>() { return Ok(Value::Bool(b.is_true())); } // int 判定 if let Ok(i) = obj.downcast::<PyInt>() { return Ok(Value::Int(i.extract::<i64>()?)); } // float? str? list? … と続く } 先に int を見ると、True が Int(1) になって事故る エラーは出ないので、答えがひっそりと変わる 落とし穴①(同じつもりのコードが違う答えを返す)を防ぐ
注意ポイント ④ 変換関数では、i64 に収まらない int を 境界で弾く >>> 10 **
30 # Python の int に上限はない 1000000000000000000000000000000 Rust の i64 は 9,223,372,036,854,775,807 まで 収まらない値は、黙って丸めずに OverflowError で返す
注意ポイント ④ 変換関数では、i64 に収まらない int を 境界で弾く >>> 10 **
30 # Python の int に上限はない 1000000000000000000000000000000 Rust の i64 は 9,223,372,036,854,775,807 まで 収まらない値は、黙って丸めずに OverflowError で返す ⭕ 変換関数の中(int を受けるところ) fn to_value(obj: &Bound<PyAny>) -> PyResult<Value> { // … if let Ok(i) = obj.downcast::<PyInt>() { let v: i64 = i.extract()?; // 収まらなければ ? が OverflowError にして呼び出し側へ返す return Ok(Value::Int(v)); } // … }
前ページの OverflowError はどうやって Python に届くのか? • PyO3 が Python の例外にしてくれる
◦ to_value(obj: &Bound<PyAny>) -> PyResult<Value> = Result<Value, PyErr> ◦ PyErr = Python の例外を Rust 側で表す型 Rust に例外は無い。Err を返せば境界で Python の例外になる
STEP 4 maturin で Rust core を Python へ配る evaluator/
├─ core/ ✅ ├─ bindings-py/ Python への橋(PyO3) │ ├─ Cargo.toml │ ├─ src/lib.rs │ ├─ evaluator.pyi │ └─ pyproject.toml └─ bindings-wasm/ ←最後の一手間が必要
maturin とは? Rust を wheel に詰めるビルドツール • 前章までで Rust core
と Python binding を作ったが、実はまだ Python からは利用できない ◦ Rust のコードを .so にビルドし、Python パッケージの形にしないと import できない ◦ それをやるのが maturin
maturin とは? Rust を wheel に詰めるビルドツール • 前章までで Rust core
と Python binding を作ったが、実はまだ Python からは利用できない ◦ Rust のコードを .so にビルドし、Python パッケージの形にしないと import できない ◦ それをやるのが maturin core + bindings-py • • build 配れる一つのファイル maturin は setuptools と同じビルドバックエンドの位置 ◦ import evaluator wheel (.whl) Rust のコード cargo build 後に生成された .so を wheel に詰める maturin build --release コマンドで wheel を作れる pip install 配った先の Python サーバ
maturin の実行前に必要な設定ファイルは 2 つだけ ① pyproject.toml(ビルド方法の宣言) [build-system] requires = ["maturin>=1.7,<2"]
build-backend = "maturin" [project] name = "evaluator" version = "0.1.0" requires-python = ">=3.9" ② Cargo.toml(cdylib として吐く) [lib] name = "evaluator" crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.22", features = ["abi3-py39"] } evaluator-core = { path = "../core" } build-backend = "maturin" の 1 行が、pip と cargo をつないでくれる
注意ポイント ⑤ 型スタブ(.pyi)は、maturin build だけでは付いてこない
注意ポイント ⑤ 型スタブ(.pyi)は、maturin build だけでは付いてこない ※生成ツールもある: pyo3-stub-gen / maturin 1.13+
の --generate-stubs(PyO3 の experimental-inspect が必要・開発中)今回は関数 1 つなので手書き 型スタブは自分で用意する 置き場所さえ合っていれば同梱は maturin がやってくれる
おまけ Wasm への展開 evaluator/ ├─ core/ ✅ ├─ bindings-py/ ✅
└─ bindings-wasm/ ├─ Cargo.toml └─ src/lib.rs ブラウザへの橋(wasm-bindgen)
再掲
新しく足すのは bindings-wasm だけ
新しく足すのは bindings-wasm だけ 新しく書くのはこの1ファイルだけ // bindings-wasm/src/lib.rs use evaluator_core as core;
use wasm_bindgen::prelude::*; binding 追加 #[wasm_bindgen] pub fn evaluate(expr_json: &str, input_json: &str) -> Result<String, JsValue> { core::evaluate(..) // PyO3 と同様に Rust core を呼ぶ } Rust core に配布先(Python)の都合を入れなかったので、 Wasm 対応は binding を足すだけで済んだ
まとめ
各セクションの注意ポイントまとめ ① core 公開 • core に属性マクロを持ち込まない ② 変換層 •
• • 型は Python にも JavaScript にも寄せず、最大公約数で決める bool を int より先に判定する 収まらない値は、黙って丸めずに境界で弾く ③ 配布 • maturin build で型スタブは作られない
Rust core 化で守りたかったのは、速さより正本 • Rust の速さも魅力ではある ◦ ただ、今回いちばん重視したのは、評価ロジックの正本を 1 つに保てること
Rust core 化で守りたかったのは、速さより正本 • Rust の速さも魅力ではある ◦ • ただ、今回いちばん重視したのは、評価ロジックの正本を 1
つに保てること Python だけで完結するなら、Python のままでいい ◦ 同じロジックを Python 以外の言語でも動かしたいのであれば、 Rust core 化は有力な選択肢になる ◦ ただし、橋のコードは自分で書く(= 今日の注意ポイント 5 つ)
None
Thank you!