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

出典: Mojo Manual — python

補足: この章では、「同じもの」として見るより「橋を渡す」と捉えるほうが整理しやすいでしょう。

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 を呼ぶ回数を減らすことが重要です。

詳細: Calling Python from Mojo

出典: 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 は、なるべくわかりやすく薄く保つほうが扱いやすいです。

詳細: Calling Mojo from Python

出典: 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 を使うための章 と捉えるとよいでしょう。