13. Python 相互運用
13.1. この章で学ぶこと
Mojo から Python を呼ぶ方法
Python から Mojo を呼ぶ方法
型の境界とオブジェクトの受け渡し方
この章のテーマは、既存の Python 資産をどう活かすか です。
Mojo は Python に似ていますが、同じ言語ではありません。 そのため、ここでは Mojo と Python をどうつなぐか を見ます。
まずは次の3つを押さえると読みやすいです。
Mojo から Python を使える
Python から Mojo を呼ぶこともできる
その間には型や変換の境界がある
13.2. Python interop
この章でいちばん大事なのは、Mojo と Python は双方向につながる ということです。
13.2.1. 何ができるのか
Mojo から Python のモジュールを使う
Python から Mojo の機能を呼ぶ
NumPy など既存の Python 資産を活かす
つまり、Python の資産を捨てずに Mojo を使える ということです。
13.2.2. どう考えるとよいか
ここでのポイントは、 Mojo は Python の延長ではなく、Python と橋をかけられる言語 だということです。
見た目が似ていても、型や実行の考え方は違います。 そのため、つなぐときには変換や境界を意識する必要があります。
詳細: Python interop
補足: この章では、「同じもの」として見るより「橋を渡す」と捉えるほうが整理しやすいでしょう。
13.3. Calling Python from Mojo
まずは、Mojo から Python を呼ぶ 場面です。
13.3.1. 基本
Mojo では、Python モジュールを読み込んで、その関数やオブジェクトを使えます。
たとえば次のような流れです。
Python.import_moduleで Python モジュールを取得する関数や属性にアクセスする
必要に応じて結果を受け取る
13.3.2. コード例(標準ライブラリ math)
次の例は、標準ライブラリの math.sqrt を Mojo から呼び、戻り値を Float64 に変換して表示します。外部の .py ファイルは不要で、Python 本体が利用できる Mojo 実行環境が前提です。
# Python 標準ライブラリの `math` を Mojo から呼び出す最小例。
from std.python import Python, PythonObject
def main() raises:
var math = Python.import_module("math")
var two = PythonObject(2.0)
var py_sqrt = math.sqrt(two)
var x = Float64(py=py_sqrt)
print(x)
リスト-1: python_from_mojo_math.mojo
Python.import_module("math")で Python 側のモジュールオブジェクトを取得する引数・戻り値は、多くの場合
PythonObjectとして受け渡しするMojo の数値型として使うには、
Float64(py=...)のように 明示的な変換が必要になる
13.3.3. PythonObject とは
Python 側の値を扱うときには、PythonObject が出てきます。
これは、Python のオブジェクトを Mojo 側で受け取るための入れ物 と捉えるとよいでしょう。
ここで大事なのは、 Mojo は静的型、Python は動的型 なので、その境目をはっきり意識することです。
13.3.4. コード例(PythonObject の生成と Mojo 型への変換)
Mojo 側で PythonObject(...) を作り、String(py=...) などのコンストラクタで Mojo のプリミティブ型へ変換する流れです(公式ドキュメントのパターンに沿った最小例)。
# `PythonObject` で値を包み、Mojo のプリミティブ型へ明示的に変換する例。
from std.python import PythonObject
def main() raises:
var py_string = PythonObject("Hello, Mojo!")
var py_bool = PythonObject(True)
var py_int = PythonObject(123)
var py_float = PythonObject(3.14)
var mojo_string = String(py=py_string)
var mojo_bool = Bool(py=py_bool)
var mojo_int = Int(py=py_int)
var mojo_float = Float64(py=py_float)
print(mojo_string)
print(mojo_bool)
print(mojo_int)
print(mojo_float)
リスト-2: python_object_wrap_and_convert.mojo
13.3.5. 速くならない部分
Python を呼んだ部分は CPython の世界に戻るため、そこは Mojo の性能の恩恵を受けません。
GIL の影響がそのまま残る
PythonObjectの変換・受け渡し自体にコストが生じる呼び出しを増やすほど Mojo 側の利点が薄れる
「Mojo を使えば Python 呼び出しも速くなる」は誤りです。 速さを得たいなら、ホットパスを Mojo 側に留め、Python を呼ぶ回数を減らすことが重要です。
出典: Mojo Manual — python-from-mojo
補足: Python を呼べるのは強みですが、ホットパスを全部 Python に寄せると Mojo の利点が薄れます。
13.4. Calling Mojo from Python
次は、Python から Mojo を呼ぶ 場面です。
13.4.1. 何をするのか
Mojo のコードを拡張モジュールとしてビルドし、 Python 側から読み込める形にします。
すると、Python から Mojo の処理を呼び出せます。
13.4.2. コード例(PyInit_ と PythonModuleBuilder)
Python が import mojo_module するとき、PyInit_mojo_module というエントリポイントを探します。そこで PythonModuleBuilder を使い、公開する関数(この例では factorial)を登録します。
Mojo 側(拡張モジュールの定義):
# Python から `import mojo_module` できるようにする拡張モジュールの例。
from std.python import PythonObject
from std.python.bindings import PythonModuleBuilder
from std.os import abort
@export
def PyInit_mojo_module() -> PythonObject:
try:
var m = PythonModuleBuilder("mojo_module")
m.def_function[factorial]("factorial", docstring="Compute n!")
return m.finalize()
except e:
abort(String("error creating Python Mojo module:", e))
def factorial(py_obj: PythonObject) raises -> PythonObject:
var n = Int(py=py_obj)
var result = 1
for i in range(2, n + 1):
result *= i
return PythonObject(result)
リスト-3: mojo_module.mojo
factorialは引数をPythonObjectで受け取り、Int(py=...)で Mojo のIntに変換する。戻り値は再びPythonObjectに包んで Python 側へ返す
Python 側(呼び出し):
# Mojo でビルドした `mojo_module` を Python から読み込む例。
# 実行前に、このディレクトリを PYTHONPATH に含め、Mojo 拡張をビルドしておくこと。
import mojo.importer
import mojo_module
print(mojo_module.factorial(5))
リスト-4: call_mojo_factorial.py
実際に動かすには、Mojo のビルド手順(モジュール名と出力パス、PYTHONPATH、必要なら mojo.importer のセットアップ)を、Calling Mojo from Python の最新手順に沿ってください。リリースごとにコマンドやレイアウトが変わり得ます。
13.4.3. どう考えるとよいか
この方向では、 重い処理を Mojo に任せて使い慣れた Python から呼ぶ という使い方がしやすいです。
つまり、
Python は使いやすい入口
Mojo は速く動かしたい中身
という分担を考えやすくなります。
13.4.4. 注意点
この仕組みは便利ですが、ビルド手順や対応状況は変わりやすいことがあります。 そのため、実際に使うときはマニュアルとリリースノートを確認するのが安全です。
また、Python 側に見せる API は、なるべくわかりやすく薄く保つほうが扱いやすいです。
出典: Mojo Manual — mojo-from-python
補足: 配布方法やビルド周りは更新が早いので、実際に作るときは公式確認が前提です。
13.5. Python types
最後に、型の境界 を見ます。
13.5.1. 基本
Python のオブジェクトを Mojo で扱うときは、PythonObject が中心になります。
これは、Python の値をそのまま静的型の値として扱うのではなく、いったん橋渡し用の形で持つ ための仕組みです。
13.5.2. 何が起こるのか
この境界では、次のようなことを意識する必要があります。
Mojo 型へ変換する
Python のオブジェクトとして扱う
属性へアクセスする
必要ならカスタム型を Python 側へ渡す
ここで大事なのは、型変換や受け渡しにはコストやルールがある ことです。
見た目が簡単でも、内部では変換や橋渡しが入ることがあります。
リスト-1(Float64(py=...))とリスト-2(String(py=...) など)は、この境界を越えるときの典型パターンです。
13.5.3. どう読むとよいか
この節は、細かな API 名を覚えるよりも、 静的型の Mojo と動的な Python の間に境界がある と理解するのが先です。
そこがわかると、なぜ PythonObject のような仕組みが必要なのか見えやすくなります。
詳細: Python types
出典: Mojo Manual — python/types
補足: ここは、Mojo と Python の違いが最も見えやすい場所です。
13.6. この章を一文で言うと
Mojo と Python は双方向につながるが、その間には型と変換の境界があるので、それを意識して使うことが大切です。
13.7. まとめ
Mojo から Python のモジュールや関数を呼べる
Python から Mojo の処理を呼ぶこともできる
PythonObjectは Python 側の値を扱う中心になる静的型の Mojo と動的な Python の間には境界がある
変換やコピーのコストを意識すると設計しやすい
速さが必要な部分では、境界のまたぎ方を減らすのが大事
この章は、Python を捨てるための話ではなく、Python を活かしながら Mojo を使うための章 と捉えるとよいでしょう。