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)) # 5PythonObject 上的操作是动态分发的(走 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 侧直接 importpython
# 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 项目」的正规姿势
- 渐进式重写:热点函数逐个替换,全程可回滚
- 铁律:循环里不跨边界
下一篇:实战——手写矩阵乘法并对比性能。