ライブラリはPyO3でPython内からRustを動かせるが、速度を左右するのは境界だ
あるパーサーの例が、ネイティブ速度だけではPythonライブラリの高速化を保証できない理由を示したことで、PyO3をめぐる開発者の議論が新たに活発化している。
9月13日に示されたこの実演では、手作りのJSONパーサーを題材に、ライブラリがPyO3を通じてPython内でRustを動かす仕組みを説明している。まずRustが入力を解析する。次にPyO3が生成されたツリーを、辞書、リスト、文字列、数値、真偽値、Python例外へと変換する。
対立が生じるのはこの2番目の工程だ。高速なRustアルゴリズムは、統合レイヤーの処理が終わる前に完了する可能性がある。大きな結果では、ネイティブ値をPythonオブジェクトへ変換する時間が、開発者が高速化したかった処理そのものを上回り得る。
これは新しいPython機能でも、新しいPyO3リリースでもない。CPythonはC、C++、Fortranで書かれたモジュールを含め、数十年にわたりネイティブ拡張をサポートしてきた。現在の関心は、Rustがこの古いアーキテクチャを新世代のライブラリ作者にとって魅力的なものにしたことを反映している。
Pydantic、Polars、cryptographyなどのプロジェクトは、すでに馴染み深いPythonインターフェースの裏側にRustを採用している。こうした成功は、メンテナーに遅い内部パスの見直しを促す一方、パッケージ配布やプラットフォーム対応について、より難しい問いも投げかけている。
したがって重要な競争は、Rust対Pythonではない。ネイティブ計算対境界オーバーヘッドだ。この競争が、意味のある改善を生む移植と、複雑さをコンパイル済みパッケージへ移すだけの移植を分ける。
ライブラリは馴染み深いimportを通じてPyO3でPython内からRustを動かす
PyO3は、アプリケーション開発者にPython構文を捨てさせることなく、コンパイル済みRustをCPythonがimportできるネイティブ拡張へ変換する。
Bob Belderbosは、Rustで書かれPython関数として公開されたJSONパーサーで、この経路を実演した。彼のパーサー解説は、このプロセスを4段階に整理している。
開発者は通常のRustモジュールを書き、PyO3属性を追加し、maturinでパッケージをビルドして、生成された拡張をPythonからimportする。Maturinは、RustベースのPythonモジュール向けのビルド・パッケージングツールだ。
コンパイル済み成果物は、共有ライブラリに含まれるネイティブなマシンコードである。オペレーティングシステムに応じて、そのファイルの拡張子は一般に.so、.dylib、または.dllとなる。Pythonは、従来のネイティブモジュールでも使われてきた広範な拡張機構を通じてこれを読み込む。
この表現は重要だ。Pythonは実行時にRustソースコードを解釈するわけではない。Rustコンパイラがマシンコードを生成し、CPythonはネイティブのアプリケーション・バイナリ・インターフェースを通じて公開関数を呼び出す。
PyO3はバインディングレイヤーを提供する。そのマクロは、関数呼び出し、参照管理、引数抽出、戻り値、例外処理に必要な接着コードの多くを生成する。
この実演では、#[pyfunction]がPythonから呼び出せるRust関数を指定する。#[pymodule]マクロは、import時にPythonが初期化する拡張モジュールを定義する。
利用者から見える体験は通常のPythonのままだ。呼び出し側はモジュールをimportし、パース関数に文字列を渡す。このやり取りにおいて、呼び出し側がRustの所有権、trait、ライフタイム、Cargoを理解する必要はない。
実装は異なる経路を取る。入力はPython文字列からRust文字列参照へと渡る。Rustがパースを実行し、JSONツリーを表すenumを構築する。
enumは、定義済みの複数のバリアントのいずれかを保持できるRustの型だ。この場合、それらのバリアントはnull、真偽値、数値、文字列、配列、オブジェクトを表す。
生成されたツリーは当初、完全にRust側に属している。PythonのインタープリターはPythonのメモリー・型システムに従うオブジェクトを想定するため、この構造を直接利用できない。
PyO3の公式ガイドは、プロジェクトがサポートする両方向を説明している。開発者はRustでPythonモジュールを作成することも、Rustアプリケーション内にPythonインタープリターを埋め込むこともできる。
この話を支えているのは前者の方向だ。メンテナーはPython向けインターフェースを維持しながら、選択した処理をコンパイル済みコードへ移せる。
このモデルはすでにPythonエコシステム全体で一般的だ。NumPyは、ネイティブ計算を基盤としつつ便利なPython操作を提供することで、より大きなパターンを確立した。PyO3が変えるのは拡張を構築する言語とツール群であり、基本アーキテクチャではない。
今回の議論が重要なのは、隠れていた境界を可視化するからだ。注目すべき出来事は、Pythonが突然ネイティブコードを実行できるようになったことではない。より多くのメンテナーがCPython向けの接着コードを各層で手書きせずに、こうした拡張を構築できるようになったことだ。
この実装上の障壁の低下により、ネイティブ移植を検討する価値のある関数の範囲が広がる。ただし、Rustに入るものと出るものすべてを含めた、呼び出し全体を測定する必要はなくならない。
PythonメンテナーはホットパスをRustへ移す圧力に直面している
Rustを基盤にしたパッケージの成功により、ネイティブ拡張は専門技術から、主流のPythonプロジェクトにとって現実的な保守戦略へと変わった。
最も明確な参照点はPydanticだ。同プロジェクトの2番目のメジャーバージョンでは、バリデーション処理をRustで実装された別パッケージpydantic-coreへ移した。
プロジェクトの初期のPydantic V2設計では、書き換えられたコアが第1バージョンを大幅に上回る性能を実現したとしている。これらの数値はPydantic自身のプレリリースベンチマークによるものであり、結果は依然としてワークロードによって決まる。
アーキテクチャ上の示唆は、単一のベンチマークより重要だ。Pythonコードがモデルを定義してスキーマを生成し、Rustコアが性能に敏感なパスでバリデーションとシリアライズを実行する。
Polarsは同じパターンをより広く適用している。そのコアはRustで書かれ、Pythonを含む複数言語向けのインターフェースを通じて公開されている。
プロジェクトのPolarsアーキテクチャでは、クエリ計画、列指向操作、並列実行がネイティブのデータ構造に近い位置に保たれている。Pythonユーザーは引き続き式を書き、馴染み深い高水準の結果を受け取る。
これらのプロジェクトは、パーサー、バリデーター、トークナイザー、圧縮ツール、データベースクライアント、データエンジンのメンテナーに影響を与えている。ユーザーは現在、Pythonパッケージが親しみやすいインターフェースを保ちながら、選択した内部実装を置き換えられることを知っている。
この圧力は単にベンチマーク順位に関するものではない。ネイティブコードは、明確に定義された操作においてCPU時間を削減し、スループットを向上させ、より予測可能なメモリー利用を実現できる。
Rustはメンテナーに別の魅力も提供する。その所有権・型システムは、メモリーエラーの一部をコンパイル時に検出する。ただし、unsafeコードや依存関係の欠陥が残る可能性はある。
PyO3は、多くの一般的なPython型とRust型の間の変換も提供する。RustのエラーをPython例外へ変換し、Pythonの参照管理にも関与する。
このツール群により、拡張は手書きのCインターフェースより保守しやすくなる場合がある。ネイティブ統合を自動化するわけではないが、必要となる独自機構の量を減らす。
その結果、メンテナーにとってビルド対購入の判断が変わる。以前であれば、遅いPython関数は、ネイティブへの書き換えに希少なCの専門知識が必要だったため、Pythonのまま残される可能性があった。
現在では、Rustの経験を持つチームがPyO3を通じてより小さなネイティブコアを公開できる。Maturinはその拡張をビルドし、Pythonパッケージ内に配置できる。
この経路は、狭い関数がコンパクトな入力を受け取り、十分な計算を実行し、コンパクトな結果を返す場合に最も適している。圧縮、ハッシュ、パースの要約、バリデーション、数値カーネルはしばしばこの形に合う。
一方、関数が言語境界を繰り返し越える場合は、予測しにくい。多数の小さな呼び出しでは、引数チェック、ディスパッチ、割り当て、変換によって時間を失う可能性がある。
大きな結果は、関連する問題を生む。アルゴリズムはRustで効率的に実行されても、呼び出し側は依然として通常のPythonオブジェクトを期待する。
この期待により、データ表現が決定要因となる。列指向バッファーをネイティブメモリーに保持するライブラリは、数千のネストした辞書を返すパーサーとは異なるコスト構造を持つ。
したがってメンテナーは、単なる言語の書き換えではなく、アーキテクチャ上の判断を迫られている。どちらの側がデータを所有するのか、Pythonオブジェクトをいつ存在させるべきかを決めなければならない。
ユーザーはエンドツーエンドの挙動を比較するため、この圧力は続くだろう。彼らが重視するのは、関数を呼び出してから利用可能な結果を受け取るまでの時間であり、内側のループだけを切り出した速度ではない。
戻りの変換でネイティブ速度の利点が失われる可能性がある
中心となる仕組みはオブジェクトの実体化だ。大きなRustの結果をPythonオブジェクトへ変換する処理が、完了した操作全体を支配することがある。
Belderbosのパーサーはまず、JSONドキュメント内のすべての値を含むRustツリーを作成する。この段階では、Rustのコンパイル済み実行と明示的なメモリーモデルの恩恵を受けられる。
次の段階では、ツリー全体を再び走査する。各RustオブジェクトはPython辞書になり、各配列はリストになり、各リーフはPython値になる。
この処理は実体化と呼ばれる。呼び出し側が検査、変更、シリアライズ、あるいは他へ渡すことを期待する具体的なPythonオブジェクトを作成する。
PyO3のIntoPyObject traitがこの変換を調整する。traitは型が実装できる振る舞いを定義し、各JSONバリアントが対応するPython表現を記述できるようにする。
変換は再帰的だ。オブジェクトには新しい辞書が必要であり、その後に各キーと値のエントリーが必要となる。配列には、変換済みの子要素を含むリストが必要だ。
各PythonオブジェクトはCPythonのメモリー管理システムにも入る。割り当て、型メタデータ、参照カウントには、Rustツリーには存在しなかったコストが伴う。
従来のCPythonビルドには、もう一つの制約がある。Pythonオブジェクトに触れるコードには通常、グローバルインタープリターロック、すなわちGILに関連するインタープリターへのアクセスが必要だ。
GILにより、従来のPythonインタープリターを一度に動かせるスレッドは1つに限られる。Pythonオブジェクトから切り離されたRust処理は、そのロックを保持せずに実行できる。
PyO3は、Rustが独立した計算を行う間にインタープリターへのアクセスを解放するPython::detachの仕組みを文書化している。その並列実行ガイドでは、他のPythonスレッドがその間に処理を進められる仕組みを説明している。
この手法が役立つのは、Rustの処理がPythonオブジェクトに触れない間だけだ。Python辞書やリストを再構築すると、実行は再びインタープリター境界へ戻る。
パーサーの例では、100,000個の値を含むドキュメントには、おおよそ同程度の数のPythonオブジェクト作成が必要になると見積もっている。これは説明のための関係であり、普遍的な性能ベンチマークではない。
形状はサイズと同じくらい重要だ。フラットな数値バッファーは、共有表現を通じて境界を越えられる場合がある。深くネストしたドキュメントでは、より多くのオブジェクト割り当てとポインター走査が必要になる。
呼び出し頻度も別の軸となる。大きく連続したバッファーを処理する1回のネイティブ呼び出しは、セットアップコストを償却できる。単一の値を処理する数千回の呼び出しでは、多くの場合それができない。
エラー処理も境界を越える。Rustのパースエラーは、期待されるクラス、メッセージ、位置情報を持つPython例外へ変換される必要がある。
PyO3は型付けされたRustエラーをPyErrへマッピングできる。そのため、不正な文字列はPythonの呼び出し側にはValueErrorとして現れ、利用できないファイルはFileNotFoundErrorになる可能性がある。
その翻訳には、公開 API を保護する価値があります。メンテナーが実装言語を変更したというだけで、ユーザーが別のエラーモデルを必要とするべきではありません。
ただし、Python のセマンティクスを維持するには作業が伴います。ネイティブへの書き換えでは、エッジケース、例外の型、イテレーションの挙動、所有権のルール、場合によってはサブクラスとの相互作用まで再現しなければなりません。
したがって、真の最適化対象はインターフェース全体です。表現変換が変わらず、実行時間の大半を占め続けるなら、パースだけを高速化しても十分な効果は得られません。
ライブラリには、いくつかのアーキテクチャ上の対応策があります。より小さな要約を返す、イテレーターを公開する、コールバックをバッチ単位で処理する、あるいは不透明な Rust バックエンドのオブジェクトを存続させる、といった方法です。
遅延ビューは、特に大規模なツリーにおいて重要です。すべての Python オブジェクトを即座に構築するのではなく、拡張機能は呼び出し元が要求した値だけを実体化できます。
この設計では、アプリケーションが結果の一部しか読み取らない場合に不要な変換を減らせます。一方で、複雑さはオブジェクトのライフタイム、キャッシュ、変更操作、API 設計へと移ります。
カラム型ライブラリは、データを連続したネイティブバッファに保持することで、一部のコストを回避できます。Python は各要素を複製する代わりに、基盤となるストレージを参照する軽量オブジェクトを受け取ります。
この戦略は、Polars が機械的な関数単位の書き換えよりも強力なネイティブアーキテクチャを示す理由の一つです。その Python インターフェースは、相当量の処理とデータをまとめて保持する Rust エンジンを制御します。
通常の辞書を返すパーサーは、あまり有利でない境界に直面します。その出力形式では、拡張機能が Python コードの期待するオブジェクトグラフそのものを作成する必要があります。
ここでの教訓は、PyO3 の変換が特別に非効率的だということではありません。どの Foreign Function Interface でも、両側の表現、ライフタイム、エラー、所有権を調整しなければなりません。
PyO3 は、そうした責務を表現しやすくします。しかし、そのコストをなくせるわけではありません。
Rust と Python は協力関係にあるが、パッケージングは現実を突きつける
高速な拡張機能は、コンパイル済み wheel が OS、プロセッサー、インタープリター、バイナリインターフェースに適合しなければならないため、配布上の責務を生みます。
Pure Python パッケージには、非常に大きな移植性の利点があります。単一の汎用 wheel が、複数の OS やプロセッサーアーキテクチャで動作できる場合が多いためです。
コンパイル済み拡張機能は、プラットフォーム固有のマシンコードを生成します。パッケージのメンテナーは互換性のある成果物を提供するか、ユーザーにローカルでのコンパイルを求めなければなりません。
wheel は Python のビルド済み配布形式です。インストール可能なパッケージファイルを含み、インタープリター、アプリケーションバイナリインターフェース、プラットフォームの互換性タグを持ちます。
Python packaging guide は、標準的なマトリクスを説明しています。メンテナーは多くの場合、Python バージョン、OS、アーキテクチャをまたぐビルドを必要とします。
継続的インテグレーションは、その作業の大部分を自動化できます。Maturin や関連ツールは wheel のビルドと公開を行え、cibuildwheel のようなプロジェクトは対象環境をまたぐビルドを調整します。
自動化によってテストが不要になるわけではありません。wheel は問題なくインストールできても、未対応の CPU 機能、欠落したシステムライブラリ、互換性のないランタイム要件によって失敗する場合があります。
Linux は、ディストリビューションごとに異なるバージョンのシステムコンポーネントが提供されるため、特に複雑です。Manylinux 仕様は、広く互換性のある wheel 向けに標準化されたビルド環境をプロジェクトへ提供します。
Windows と macOS には、それぞれ独自の成果物が必要です。Apple が Intel と Arm のハードウェアに分かれたことで、ユニバーサルバイナリがアーキテクチャを統合できる場合もあるとはいえ、新たな次元が加わりました。
安定 ABI は、インタープリターバージョンという次元を減らせます。ABI とは、コンパイル済みコードがバイナリランタイムを呼び出す方法を規定する低レベルの契約です。
CPython の従来の abi3 安定 ABI では、要件を満たす拡張機能はプラットフォームおよびアーキテクチャごとに 1 つの wheel で、複数の Python 3 バージョンを対象にできます。その拡張機能は Limited API の範囲に制限されなければなりません。
この制約は、一部の API へのアクセスや潜在的な最適化と引き換えに、より広い互換性を得るものです。PyO3 は、abi3 拡張機能をビルドする際に適切な最小 Python バージョンを選ぶことをサポートしています。
フリースレッド版 CPython の登場により、パッケージングの問題は再び変化しています。フリースレッド版ビルドでは従来の GIL 構成がなくなり、ネイティブ拡張機能が前提としてきた仮定が変わります。
現在の PyO3 ドキュメントでは、従来の abi3 wheel と、フリースレッド版ビルド向けの新しい安定 ABI パスを区別しています。メンテナーは、自身の成果物が実際にどのインタープリター構成をサポートするか検証する必要があります。
パーサーに関する記事をめぐる公開討論では、プラットフォームの問題がすぐに浮上しました。開発者たちは、Rust バックエンドの依存関係が Python を実行できるあらゆる環境で動作するのかを問いました。
簡潔に言えば、答えはいいえです。コンパイル済み依存関係は、そのメンテナーが互換性のある wheel を公開しているか、ユーザーが対応ツールチェーンでビルドできる環境でのみ動作します。
この制約は、他の言語で書かれたネイティブ拡張機能にも当てはまります。Rust は利用可能なコンパイラーターゲットとビルド依存関係を変えますが、基本的な移植性の問題を解決するわけではありません。
cryptography パッケージは、ユーザー体験をよく示しています。そのドキュメントでは、大半のユーザーはビルド済み wheel を受け取り、Rust をインストールする必要はないと説明されています。
公開された wheel の対象外にいるユーザーは、Rust コンパイラーやその他のネイティブ依存関係を必要とする場合があります。このフォールバックは、pip install が言語に依存しないままであると期待していた人々を驚かせるかもしれません。
ブラウザでホストされる Python は、もう一つの端的な例です。Pyodide は WebAssembly を通じて Python を実行するため、従来のデスクトップ向け・サーバー向け wheel は自動的には機能しません。
エコシステムでは、Rust を含むパッケージ向けの WebAssembly ビルドが進展しています。Hacker News の議論で挙げられたリンクによれば、Pydantic-core は関連する成果物を提供しています。
それでも、WebAssembly のサポートには意図的なパッケージングとテストが必要です。入出力の挙動も、スレッド、ファイル、ソケット、非同期ランタイムに関するブラウザの制約に直面する可能性があります。
モバイルシステム、組み込み環境、特殊なプロセッサー、古いエンタープライズディストリビューションも、同様の負荷を生みます。Pure Python のフォールバックは対応範囲を守れますが、2 つの実装を維持する作業が加わります。
そのためライブラリチームは、ネイティブへの書き換えごとに移植性のコストを織り込む必要があります。コードパスは高速化できても、プロジェクト全体の利用者へ配布する難しさは増す可能性があります。
人気のあるパッケージでは、このトレードオフを受け入れる価値がある場合があります。大規模なコントリビューターベースと成熟したリリースパイプラインが、広範な wheel マトリクスを支えられるためです。
小規模なプロジェクトでは、異なる計算式になります。ネイティブビルドにより、コンパクトなライブラリが複数環境にまたがるリリースエンジニアリングの責務へと変わる可能性があります。
PyO3 と maturin は、その責務を大幅に軽減します。しかし完全になくすわけではなく、ユーザーはビルドされていないすべてのターゲットをインストール失敗として体験します。
懐疑的な検討はエンドツーエンドの計測から始まる
「Rust で書かれている」は実装上の事実であり、性能の結果ではありません。メンテナーには変換と利用を含むベンチマークが必要です。
Rust は、同等の Python ループと比べて CPU 集約型コードの速度を向上させることがよくあります。しかし、この比較だけではアプリケーションの完了したワークロードについてほとんど何も分かりません。
拡張機能の呼び出しには、引数変換、検証、ネイティブディスパッチ、計算、出力変換、メモリー確保、エラー処理が含まれます。呼び出し元はその後、結果を再び変換することもあります。
有用なベンチマークは、その経路全体を測定します。アプリケーションが使用する表現から始め、次の段階で利用できるデータで終えます。
マイクロベンチマークにも役割はあります。どの段階が時間を消費しているかを特定し、コード変更後にアルゴリズムが改善されたかを明らかにできます。
最速の内部段階だけを示すと、誤解を招きます。パーサーのスループット値は、その後に大きな Python オブジェクトグラフを構築するコストを隠すことがあります。
ベンチマークには代表的な入力も必要です。小さな文書は固定的な呼び出しオーバーヘッドを過大評価し得る一方、均一な合成データでは本番環境のメモリー確保パターンを捉え損ねる場合があります。
ウォームキャッシュ、リリースビルド、コンパイラーフラグ、CPU 機能はいずれも結果を変え得ます。比較ではこうした条件を明示し、同じ公開挙動をテストするべきです。
正確性も同等に重視されるべきです。パーサーとバリデーターは、不正なエンコーディング、極端なネスト、重複キー、数値のエッジケース、予期しないリソース使用に遭遇します。
例外を変えたり、異なる入力を受け入れたりする移植版は、異なる処理を行うことで高速になっている可能性があります。互換性テストは性能テストに付随しなければなりません。
メモリー消費は、一見した優位性を覆すことがあります。完全な Rust ツリーと、新たに実体化された Python ツリーの両方を保持すると、一時的に 2 つの表現が必要になる可能性があります。
ストリーミングや遅延設計は、この重複を減らせます。ただし、エラーが発生するタイミングやネイティブバッファが割り当てられたままになる期間も変え得ます。
並行性に関する主張にも同様の注意が必要です。Rust コードは複数スレッドを利用でき、PyO3 は適切なネイティブ処理中にインタープリターへのアクセスを解放できます。
拡張機能は、任意のネイティブスレッドから通常の Python オブジェクトを安全に操作することはできません。これらとやり取りする際には、PyO3 のインタープリタールールに戻る必要があります。
フリースレッド版 Python はこの状況の一部を変えますが、アプリケーションデータから同期を不要にするわけではありません。スレッド安全性は引き続きライブラリの明示的な責務です。
セキュリティも単純なスローガンには収まりません。Rust の safe サブセットは複数のメモリー誤用カテゴリを防ぎますが、ネイティブ拡張機能には unsafe ブロックや脆弱な依存関係が含まれる可能性があります。
コンパイル済みコードの欠陥は、通常の Python 例外を発生させるのではなく、インタープリタープロセスをクラッシュさせる場合があります。この結果は、すべてのネイティブ拡張言語に当てはまります。
したがって、PyO3 を支持する最も強い根拠は具体的です。計算量とデータレイアウトがその境界を正当化する場合に、メンテナーは Python インターフェースと Rust 実装を組み合わせられます。
最も弱い根拠は、表面的な書き換えです。関数の処理量が限られている、あるいは時間の大部分を I/O に費やしている場合、簡潔な Python コードをネイティブ機構に置き換えても得るものはほとんどありません。
プロジェクトはまず、既存アプリケーションをプロファイリングするべきです。安定したホットパスを特定し、互換性要件を定義し、受け渡す値の量と形を測定する必要があります。
次にチームは、1 つの境界を試作できます。変換が支配的であれば、次の判断はパーサーの命令やコンパイラー最適化ではなく、データ表現に関するものになります。
この懐疑的な検証は、Rust バックエンドの Python に反対するものではありません。このアプローチが、あらゆる性能上の不満に対する既定の答えになることを防ぎます。
成功したネイティブ拡張機能は、通常のユーザーからその実装を隠します。インストールは機能し、例外は認識可能なまま、型ヒントは有用であり、実際のワークロードで性能が向上します。
ユーザーが言語選択を称賛する必要はありません。既存の Python 操作がより早く終わることに気づくだけでよいのです。
PyO3 の次の展開を示す 3 つの兆候
次の段階は、境界を意識した API、より広い wheel 対応範囲、従来型とフリースレッド版の両方の Python で動作する拡張機能にかかっています。
最初の兆候は、公開ベンチマークの変化です。より多くのプロジェクトが、ネイティブカーネルのスループットだけでなく、オブジェクト作成を含むエンドツーエンドの計測結果を報告するようになるでしょう。
この変化は、言語選択を観測可能なアプリケーション結果と結び付けるため、PyO3 を支持する根拠を強めます。同時に、変換時に利得が消える移植版も明らかにします。
複数の戻り値設計を比較するベンチマークに注目してください。ネイティブバックエンドのビュー、イテレーター、バッチ結果、完全に実体化された辞書では、結果が大きく異なる可能性があります。
メモリープロファイルも、計測結果と並んで示されるべきです。拡張機能が Rust と Python の重複した構造を一時的に保持しているかを確認できます。
2 つ目の兆候は、より広い wheel 対応範囲です。成功するプロジェクトは、主要な OS、プロセッサーファミリー、サポート対象の Python バージョン向けに、信頼できる成果物を公開するでしょう。
WebAssemblyには特に注意を払うべきです。ツールチェーンの改善と、PyPIで配布されるWebAssembly wheelの拡充により、ブラウザベースのPythonユーザーが指摘する移植性への懸念を和らげられます。
モバイル環境や比較的利用者の少ないLinuxアーキテクチャも、有用な検証対象です。対応ターゲットが一つ増えるごとに、「通常のPython依存パッケージ」が実際に意味する範囲が広がります。
プロジェクトはソースビルドの手順も文書化すべきです。エラーメッセージ、必要なコンパイラ、再現可能なビルド手順が明確であれば、wheelがないことによる影響は小さくなります。
ネイティブパッケージが、純粋なPython版では対応していたターゲットを繰り返し切り捨てるなら、移植性への懸念は強まります。ビルド自動化によってその差を埋められるなら、懸念は弱まります。
3つ目の指標は、free-threaded CPythonへの安定した対応です。拡張機能は、インタープリタロック、共有状態、オブジェクトアクセスに関する前提を見直さなければなりません。
PyO3の進化するAPIとstable ABIのサポートが、この移行を左右します。メンテナーに必要なのはコンパイルの成功だけではありません。現実的なワークロードでの並行性テストも必要です。
成熟した到達点は、リリースの複雑さを制御不能なほど増やすことなく、従来のインタープリタとfree-threadedインタープリタの両方を一つのプロジェクトでサポートできる状態です。
失敗の形は異なります。断片化したwheelセット、不明確なABIタグ、あるいは隠れたシリアライゼーションのボトルネックは、Rustバックエンドのパッケージを標準的な依存関係として信頼しにくくします。
大きな方向性には、依然として説得力があります。Pythonは大規模なユーザーベース、読みやすいオーケストレーション、生産性の高いアプリケーション層を提供します。Rustはコンパイル済みのカーネル、明示的なデータ構造、より安全なネイティブ向けツールチェーンを提供します。
ただし、この連携が成功するのは、その境界を設計の一部として扱う場合に限られます。ライブラリはPyO3を使ってPython内でRustを実行しますが、ユーザーが利用するのはPythonオブジェクトとPythonパッケージの成果物です。
移植を検討する開発者は、具体的な一つの問いから始めるべきです。高速なRust関数が戻ったあと、その境界を越えなければならないものは何か。
書き換えに踏み切る前に、その戻り値の経路をプロファイルしてください。約束したすべてのターゲットでwheelをテストしてください。そのうえで、ユーザーがすでに依存しているPython実装と、処理全体を比較してください。
ネイティブ版がなお優位なら、PyO3はその役割を果たしています。変換処理が性能向上分を食いつぶすなら、コードをさらに変更する前にインターフェースを見直してください。



