Skip to content
当前页大纲

Mojo 与 Python 互操作

本篇覆盖:在 Mojo 里调 Python(import 模块、传值、numpy 协作)、反向在 Python 里调 Mojo、渐进式重写的实践策略。互操作是 Mojo 区别于所有「Python 杀手」的核心卖点:不是替代 Python,而是长在 Python 上。

一、在 Mojo 里调用 Python 模块

通过 Python 对象导入任意已安装的 Python 模块:

mojo
from std.python import Python

def main() raises:
    var np = Python.import_module("numpy")
    var ar = np.arange(15).reshape(3, 5)
    print(ar)
    print(ar.shape)

输出和 Python 里一模一样:

[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]]
(3, 5)

几个要点:

  • 被调用的模块(numpy)必须安装在当前环境中(pixi/uv 装的同一环境)
  • 注意 main 声明了 raises——跨 Python 边界的调用可能抛 Python 异常,Mojo 要求显式标注
  • Python.import_module() 有缓存,重复导入同一模块不会重复初始化

二、PythonObject:跨越边界的值

Python 传回来的值是 PythonObject 类型——一个通用包装,可以:

mojo
from std.python import Python, PythonObject

def main() raises:
    var np = Python.import_module("numpy")

    var arr = np.array([1, 2, 3, 4, 5])    # PythonObject
    var mean = arr.mean()                    # 调 Python 方法
    print(mean)                              # 3.0

    # 属性访问、下标、迭代都自然支持
    print(arr[0])                            # 1
    print(len(arr))                          # 5

PythonObject 上的操作是动态分发的(走 Python 解释器),性能等同于在 Python 里执行——这是设计使然:互操作边界内是 Python 语义,边界外是 Mojo 语义。

三、数值数组的高效互通:copy_to_numpy_array

数值计算场景下,逐元素跨边界传值是性能灾难。标准库提供了 numpy 桥接工具做批量搬运

mojo
from std.python import Python
from std.python.numpy import copy_to_numpy_array

def main() raises:
    # Mojo 侧的 List
    var temps: List[Float64] = [20.5, 22.3, 19.8, 25.1]

    # 整块拷给 numpy(一次内存复制,不是逐元素转换)
    var np = Python.import_module("numpy")
    var pytemps = copy_to_numpy_array(temps)

    var std_dev = np.std(pytemps)            # numpy 算标准差
    print("标准差:", std_dev)

协作模式很清晰:Mojo 负责生产/预处理数据,numpy 负责它擅长的统计运算,边界上整块搬运

四、在 Python 里调用 Mojo

反方向也通:Mojo 代码可以打包成 Python 模块被 import——这是「用 Mojo 加速现有 Python 项目」的正式姿势。

典型工作流:

bash
# 1. 把 Mojo 代码组织成包
#    mykernel/
#      __init__.mojo
#      fast_ops.mojo

# 2. 编译/安装进当前环境(pixi 项目内)
pixi add mojo
# 打包后模块随环境可用

# 3. Python 侧直接 import
python
# python 侧
from mykernel import fast_ops

result = fast_ops.vector_dot(a, b)   # 实际执行的是编译后的 Mojo 代码

对调用方来说,它就是个普通的 Python 函数——团队里其他人完全无感,只有你知道底下是编译型代码。

五、渐进式重写策略

Mojo 互操作设计的真正意图:不需要推倒重来。推荐的演进路径:

阶段 0:纯 Python 项目
阶段 1:profile 找到热点函数(90% 时间花在 5% 代码上)
阶段 2:把热点函数逐个改写成 Mojo(同一仓库、同一环境)
阶段 3:Python 侧 import Mojo 版本,替换调用点
阶段 4:热点内部继续优化——SIMD、comptime、并行

每个阶段都可验证、可回滚——对比「用 C++ 重写热点 + pybind11 胶水」的传统方案,省掉了整个胶水层和双语言构建系统。

什么时候不该用 Mojo 重写

  • 热点其实在 I/O(网络、磁盘)——语言换什么都没用
  • 热点已经有成熟库(numpy 的 BLAS、torch 的 CUDA kernel)——别跟 Fortran 争取乘
  • 代码量小且不是瓶颈——优化的维护成本 > 收益

六、互操作的成本清单

跨边界调用不是免费的,心里要有数:

操作成本
Python.import_module首次有导入开销,之后走缓存
Mojo 值 → PythonObject有包装/转换开销
PythonObject 上的每次调用走解释器,等于 Python 速度
copy_to_numpy_array 批量搬运一次 memcpy,量大时摊薄

原则:边界两侧各自批量干活,边界上只做整块搬运,永远不要在循环里跨边界

七、小结

  • Python.import_module() 让 Mojo 直接用 Python 生态(numpy/matplotlib/你自己的模块)
  • PythonObject 动态包装 Python 值;数值数组用 copy_to_numpy_array 整块互通
  • Mojo 也能被打包成 Python 模块——「加速 Python 项目」的正规姿势
  • 渐进式重写:热点函数逐个替换,全程可回滚
  • 铁律:循环里不跨边界

下一篇:实战——手写矩阵乘法并对比性能。

MIT License.