在 Python 中调用 Mojo:基于 PythonModuleBuilder 构建高性能扩展模块完整指南
2026/9/10 16:20:09 网站建设 项目流程

在 Python 中调用 Mojo:基于 PythonModuleBuilder 构建高性能扩展模块完整指南

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

如果你已经拥有一个 Python 项目,并且希望用 Mojo 的高性能计算能力加速其中关键的性能瓶颈,并不需要把整个项目用 Mojo 重写。Mojo 提供了一套内建的 Python 扩展模块机制,让你只需要把性能关键的部分用 Mojo 编写,然后像普通 Python 模块一样import并调用。本篇指南以 Mojo 官方手册中「Calling Mojo from Python」章节(Mojo/docs/site/manual/python/mojo-from-python.mdx)为核心,结合仓库内对应的可运行示例(Mojo/docs/site/code/manual/python/mojo-from-python)与 stdlib 的PythonModuleBuilder源码实现,完整讲解从最小可运行示例、类型绑定、对象构造、方法暴露、关键字/可变参数,到将 Python 代码移植为 Mojo 的实战策略,帮助你快速掌握在 Python 中调用 Mojo 的全部技术要点。

注意:从 Python 调用 Mojo 目前属于Beta 实验特性,处于早期开发阶段,API 与使用体验预计会有较多变化,文档也在持续完善中。文末列出了当前已知限制,请在使用时留意。

最小可运行示例:从 Python 导入 Mojo 模块

项目结构与最小代码

考虑如下项目结构:main.py是 Python 入口程序,mojo_module.mojo是包含供 Python 调用的函数的 Mojo 源码:

project ├── 🐍 main.py └── 🔥 mojo_module.mojo

假设我们想要一个接受 Python 值作为参数的 Mojo 函数,比如计算阶乘。Mojo 一侧的初始写法为:

def factorial(py_obj: PythonObject) raises -> PythonObject: var n = Int(py=py_obj) return std.math.factorial(n)

注意:在使用PythonObject前需要导入相关模块。仓库中的完整示例文件(Mojo/docs/site/code/manual/python/mojo-from-python/mojo_module.mojo)实际是这样导入的:

import std.math from std.os import abort from std.python import PythonObject from std.python.bindings import PythonModuleBuilder

在 Python 一侧,我们期望这样调用:

import mojo_module print(mojo_module.factorial(5))

为什么必须声明 PyInit_ 入口函数

在从 Python 调用 Mojo 函数之前,必须先让 Python 知道这个模块的存在。Python 加载mojo_module时,会寻找名为PyInit_mojo_module()的函数(如果文件叫foo.mojo,则对应寻找PyInit_foo())。在PyInit_<module>()内部,必须使用PythonModuleBuilder声明所有可从 Python 调用的 Mojo 函数与类型。

因此,完整的 Mojo 模块代码是:

from std.python import PythonObject from std.python.bindings import PythonModuleBuilder from std import math from std.os import abort @export def PyInit_mojo_module() abi("C") -> PythonObject: try: var m = PythonModuleBuilder("mojo_module") m.def_functionfactorial return m.finalize() except e: abort(String("error creating Python Mojo module:", e)) def factorial(py_obj: PythonObject) raises -> PythonObject: # 若 `py_obj` 无法转换为 Mojo `Int`,这里会抛出异常 var n = Int(py=py_obj) return math.factorial(n)

在 Python 一侧,需要把包含mojo_module.mojo的目录加入 Python 路径,然后先导入mojo.importer,再用普通的import语句加载 Mojo 代码:

import mojo.importer import mojo_module print(mojo_module.factorial(5))

运行:

python main.py

输出:

120

仓库中对应的可运行示例正是这种结构:main.py中先import mojo.importerimport mojo_module(见 main.py),并通过 Bazel 定义了modular_py_binary(名为factorial)与对应的modular_run_binary_testfactorial_test)来执行这个例子(见 BUILD.bazel)。

背后的工作原理

Python 支持一种标准的「Python 扩展模块」机制,使得编译型语言(如 Mojo、C、C++、Rust)能够以直观的方式被 Python 调用。具体来说,Python 扩展模块就是一个定义了合适的PyInit_*()函数的动态库。Mojo 内建了定义 Python 扩展模块的功能,而真正“神奇”的部分发生在导入的mojo.importer模块中。

导入 Mojo 代码后观察文件系统,会发现多了一个__mojocache__目录,内部有一个动态库(.so)文件:

project ├── main.py ├── mojo_module.mojo └── __mojocache__ └── mojo_module.hash-ABC123.so

加载mojo.importer会注册 Python 的 Mojo import hook:它在后台查找与导入模块名匹配的.mojo文件,如果找到,就使用mojo build --emit shared-lib将其编译为动态库,产物存放在__mojocache__中,并且只在缓存过期时(通常是 Mojo 源文件发生变化)才重新编译。

提示:清理缓存构建产物__mojocache__目录中应该只包含派生产物,删除其中的内容是安全的。下次导入 Mojo 模块时,需要的产物会自动重建。

导出函数的 abi 约定

@export函数必须用显式的abi效果声明其调用约定。在 Python 扩展模块中,唯一需要导出的是PyInit_<module>入口点,并且它必须使用abi("C")

@export def PyInit_mojo_module() abi("C") -> PythonObject: ...

这是因为 CPython 运行时会直接跨 C 边界定位并调用PyInit_<module>,所以它必须暴露 C 调用约定。abi("C")函数不能标记为raises,这也是为什么上面的例子在函数体内捕获所有错误并通过abort处理而不是向上传播。

相比之下,你用模块构建器注册的函数、方法和初始化器(def_functiondef_methoddef_py_init等)完全不需要@export:你通过引用把它们传给构建器,Mojo 会自动生成 CPython 真正调用的 C 包装器。这个包装器以 Mojo 调用约定调用你的函数,并把任何抛出的错误转换为 Python 异常,所以像factorial这样的注册函数可以放心地标记为raises

从 stdlib 源码(bindings.mojo)可以看到,def_function要求函数签名满足:参数类型均为PythonObject(或末尾带var **kwargs: PythonObject),返回类型必须是PythonObjectNone;不带关键字参数的函数通过 CPython 的METH_FASTCALL约定注册,接受关键字参数的函数则使用METH_VARARGS | METH_KEYWORDS

Bindings 特性:把 Mojo 类型与函数暴露给 Python

绑定 Mojo 类型

使用PythonModuleBuilder可以把任意 Mojo 类型绑定给 Python 使用。例如:

@fieldwise_init struct Person(Movable, Writable): var name: String var age: Int @export def PyInit_person_module() abi("C") -> PythonObject: try: var mb = PythonModuleBuilder("person_module") var person_type = mb.add_typePerson except e: abort("error creating Mojo module")

调用add_type()会返回一个PythonTypeBuilder,随后可以用它绑定类型构造函数(见下文「在 Python 中构造 Mojo 对象」)和方法。

任何通过PythonTypeBuilder绑定的 Mojo 类型,其对应的 Pythontype对象都会被全局注册,从而启用两个特性:

  • PythonObject(alloc=Person(..))构造包装 Mojo 值的 Python 对象,供 Python 侧使用;
  • python_obj.downcast_value_ptr[Person]()进行向下转换。

注意要被绑定到 Python 使用,Mojo 类型必须实现Writable。特定绑定特性还需要额外 trait:自定义初始化器(def_py_init)要求Movable;默认初始化器(def_init_defaultable)则同时要求DefaultableMovable

仅绑定类型还不够,还需要告诉 Python 如何与 Mojo 类型交互——先从在 Python 中构造 Mojo 对象实例开始。

在 Python 中构造 Mojo 对象

可以通过def_py_init()把 Mojo 初始化器声明为 Python 兼容的对象初始化器。例如:

@export def PyInit_person_module() abi("C") -> PythonObject: try: var mb = PythonModuleBuilder("person_module") # highlight-start _ = mb.add_typePerson.def_py_init[Person.py_init]() # highlight-end return mb.finalize() except e: abort(String("error creating Python Mojo module:", e)) @fieldwise_init struct Person(Movable, Writable): var name: String var age: Int # highlight-start @staticmethod def py_init( out self: Person, args: PythonObject, kwargs: PythonObject ) raises: # 校验参数个数 if len(args) != 2: raise Error("Person() takes exactly 2 arguments") # 把 Python 参数转换为 Mojo 类型 var name = String(args[0]) var age = Int(args[1]) self = Self(name, age) # highlight-end

有了这个绑定,就可以在 Python 中创建Person实例:

person = person_module.Person("Sarah", 32) print(person)

输出:

Person(name=Sarah, age=32)

对于支持默认构造的类型,可以使用更简单的def_init_defaultable()

var counter_type = m.add_typeCounter counter_type.def_init_defaultable[Counter]()

这使 Python 代码可以无参数创建实例:

counter = counter_module.Counter() # 创建 Counter()

「构造器」与「初始化器」的区别在 Python 中,对象构造横跨__new__()__init__()两个方法,__init__()严格来说是属性初始化器。但 Mojo struct 中没有__new__()方法,所以我们始终把__init__()称为初始化器。

将 Mojo 对象返回给 Python

从 Python 调用的 Mojo 函数,不仅需要能接受PythonObject参数,还需要能返回新值;有时甚至需要把 Mojo 原生值返回给 Python。这可以通过PythonObject(alloc=<value>)构造器实现:

def create_person() -> PythonObject: var person = Person("Sarah", 32) return PythonObject(alloc=person^)

警告如果所提供的 Mojo 对象类型之前没有通过PythonModuleBuilder.add_type()注册,PythonObject(alloc=...)会抛出异常。

从 PythonObject 转换为 Mojo 值

在处理PythonObject的 Mojo 代码中(尤其是从 Python 调用的 Mojo 函数内),通常期望参数是某个特定类型。把PythonObject变成 Mojo 原生值有两种方式:

  • 转换(Converting):把 Python 对象转换为一个逻辑值相同的新构造的 Mojo 值,由ConvertibleFromPythontrait 处理;
  • 向下转换(Downcasting):把持有 Mojo 原生值的 Python 对象,转换为指向该内部值的指针,由PythonObject.downcast_value_ptr()处理。
PythonObject 转换

许多 Mojo 类型通过ConvertibleFromPythontrait 支持从等价 Python 类型直接转换:

# 给定一个 person,克隆它并换一个名字 def create_person( name_obj: PythonObject, age_obj: PythonObject ) raises -> PythonObject: # 转换失败时会抛出异常 var name = String(name_obj) var age = Int(age_obj) return PythonObject(alloc=Person(name, age))

从 Python 调用:

person = mojo_module.create_person("John Smith")

传入非法参数会导致运行时参数错误:

person = mojo_module.create_person(42)

从源码搜索可见,ConvertibleFromPython目前已在若干基础类型上实现,包括IntBoolSIMDString等(参见 int.mojo、bool.mojo、simd.mojo 与 string.mojo),不过很多 stdlib 类型尚未实现该 trait。

PythonObject 向下转换

PythonObject值向下转换到内部 Mojo 值:

def print_age(person_obj: PythonObject) raises: # 若 `obj` 不含 Mojo `Person` 类型的实例则抛出异常 var person = person_obj.downcast_value_ptr[Person]() print("Person is", person[].age, "years old")

也支持通过向下转换进行不安全的修改。用户需要自行确保这个可变指针不与 Mojo 内部指向同一对象的其他指针发生别名:

def birthday(person_obj: PythonObject): var person = person_obj.downcast_value_ptr[Person]() person[].age += 1

完全不做类型检查的向下转换可以用unchecked_downcast_value_ptr

def get_person(person_obj: PythonObject): var person = person_obj.unchecked_downcast_value_ptr[Person]()

不检查类型的向下转换可以消除类型检查的开销,适合在已通过基准测试确认类型检查是瓶颈的紧内循环中使用。

绑定方法

绑定 Mojo 对象供 Python 使用时,可以使用PythonTypeBuilder.def_method()把选定的方法暴露给 Python。

目前,暴露给 Python 的 Mojo 方法需要相对普通 Mojo 方法做一点修改:必须是@staticmethod,且接收py_self: PythonObjectself_ptr: Pointer[Self]

from std.python import PythonObject from std.python.bindings import PythonModuleBuilder from std.os import abort @export def PyInit_mojo_module() abi("C") -> PythonObject: try: var mb = PythonModuleBuilder("mojo_module") # highlight-start _ = mb.add_typePerson .def_methodPerson.get_name .def_methodPerson.set_age # highlight-end return mb.finalize() except e: abort("error creating Mojo module") struct Person(Writable): var name: String var age: Int # highlight-start @staticmethod def get_name(py_self: PythonObject) raises -> PythonObject: var self_ptr = py_self.downcast_value_ptr[Self]() return self_ptr[].name @staticmethod def set_age( self_ptr: Pointer[mut=True, Self], new_age: PythonObject, ) raises: self_ptr[].age = Int(new_age) # highlight-end def write_to(self, mut writer: Some[Writer]): t"Person({self.name}, {self.age})".write_to(writer)

使用py_self: PythonObject可以访问 Mojo 对象实例所存放的完整PythonObject分配;而一般情况下,如果方法只是需要访问对象的字段,使用self_ptr: Pointer[Self]可以减少样板代码。从 Python 调用的 Mojo 方法目前要求非标准的 self 类型,这是受当前实现的限制,未来版本的 Python Mojo bindings 会解除。

绑定静态方法

Python Mojo bindings 支持暴露 Python 风格的@staticmethod,通过PythonTypeBuilder.def_staticmethod()绑定。用def_staticmethod()声明的函数在 Python 中可以作为类型上的静态方法调用,无需对象实例:

from std.python import PythonObject from std.python.bindings import PythonModuleBuilder from std.os import abort @export def PyInit_mojo_module() abi("C") -> PythonObject: try: var mb = PythonModuleBuilder("mojo_module") # highlight-start mb.add_typePerson .def_staticmethodPerson.is_valid_age # highlight-end return mb.finalize() except e: abort("error creating Mojo module") struct Person(Writable): var name: String var age: Int # highlight-start @staticmethod def is_valid_age(age_obj: PythonObject) raises -> PythonObject: var age = Int(age_obj) return 0 <= age <= 130 # highlight-end def write_to(self, mut writer: Some[Writer]): t"Person({self.name}, {self.age})".write_to(writer)

在 Python 中调用绑定为静态方法的 Mojo 函数,就是典型的 Python 静态方法调用:

from mojo_module import Person print(Person.is_valid_age(45)) # 输出 'True' print(Person.is_valid_age(-1)) # 输出 'False'

关键字参数

Mojo 的关键字参数有两种形式:

  1. 仅关键字参数def foo(*, x: Int)—— 目前 Python Mojo bindings不支持
  2. 可变关键字参数def foo(var **kwargs: Int)—— 在 bindings 中支持,但需要使用去糖形式def foo(kwargs: StringDict)**kwargs语法限制将在未来移除)。

可以用StringDict[PythonObject]作为最后一个参数来定义接受可变关键字参数的 Mojo 函数。简单示例:

import mojo_module result = mojo_module.sum_kwargs_ints(a=10, b=20, c=30) # 返回 60
from std.collections import StringDict def sum_kwargs_ints(kwargs: StringDict[PythonObject]) raises -> PythonObject: var total = 0 for entry in kwargs.items(): total += Int(entry.value) return PythonObject(total)

关键字参数也支持跟在普通位置参数之后;同时,获取特定关键字参数就是对StringDict做字典查找:

from std.collections import StringDict def duration_in_seconds( hours_obj: PythonObject, minutes_obj: PythonObject, kwargs: StringDict[PythonObject] ) raises -> PythonObject: var hours = Int(hours_obj) var minutes = Int(minutes_obj) var seconds = Int(kwargs["seconds"]) return hours * 3600 + minutes * 60 + seconds

在这个例子中,如果调用duration_in_seconds()时缺少必需的"seconds"命名参数,会触发运行时异常:

from mojo_module import duration_in_seconds # 传入 hours 和 minutes,缺少 "seconds" duration_in_seconds(4, 5) # ERROR: KeyError

关键字参数在绑定顶层函数、方法和静态方法时均受支持。

可变参数

Python 与 Mojo 的可变参数通常写成:

def foo(*args: Int): ...

但这个语法目前还不被 Python/Mojo bindings 支持,因为用def_function()绑定的函数只支持固定元数(fixed-arity)。

作为变通方案,可以使用更底层的def_py_function()接口,把接受可变数量参数的 Mojo 函数暴露给 Python,参数个数校验由用户自己负责:

@export def PyInit_mojo_module() abi("C") -> PythonObject: try: var b = PythonModuleBuilder("mojo_module") b.def_py_functioncount_args b.def_py_functionsum_args b.def_py_functionlookup def count_args(py_self: PythonObject, args_tuple: PythonObject) raises: return len(args_tuple) def sum_args(py_self: PythonObject, args_tuple: PythonObject) raises: var total = args_tuple[0] for i in range(1, len(args_tuple)): total += args_tuple[i] return total def lookup(py_self: PythonObject, args_tuple: PythonObject) raises: if len(args_tuple) != 2 and len(args_tuple) != 3: raise Error("lookup() expects 2 or 3 arguments") var collection = args_tuple[0] var key = args_tuple[1] try: return collection[key] except e: if len(args) == 3: return args_tuple[2] else: raise e

从 stdlib 源码可以看到,def_py_function支持两类函数签名:PyFunctionRaisingPyFunctionWithKeywordsRaising(见 bindings.mojo),最终都通过PyCFunction/PyCFunctionFast等 CPython 调用约定注册到模块中。

将 Python 移植到 Mojo 的策略:写出 Pythonic 的 Mojo

在这种绑定思路下,我们拥抱 Python 的灵活性,不试图把PythonObject参数强制塞进 Mojo 强类型系统那个狭窄受限的空间,而是直接写代码,如果写错了就让它运行时抛异常。PythonObject的灵活性带来了一种独特的编程风格:Python 代码几乎可以原样「移植」到 Mojo。

def foo(x, y, z): x[y] = int(z) x = y + z

经验法则:任何 Python 内建函数,在 Mojo 中都应该可以通过Python.<builtin>()访问。

def foo(x: PythonObject, y: PythonObject, z: PythonObject) -> PythonObject: x[y] = Python.int(z) x = y + z

构建 Mojo 扩展模块的两种方式

你可以通过以下方式创建并分发供 Python 使用的 Mojo 模块:

  • 以源码文件形式分发,通过 Python Mojo importer hook 按需编译。这种方式的优点是上手容易、项目结构简单,并且编辑后导入的 Mojo 代码总是最新的。

  • 以预编译的 Python 扩展模块.so动态库分发,使用命令编译:

    mojo build mojo_module.mojo --emit shared-lib -o mojo_module.so

    这种方式的优点是可以手动指定其他任何必要的构建选项(优化或调试标志、导入路径等),为高级用户提供了绕过 Mojo import hook 抽象层的「逃生舱」。

已知限制

Python 与 Mojo 的互操作目标远大——Mojo 希望成为扩展 Python 的最佳方式——但该特性仍处于早期且活跃的开发阶段,存在以下需要注意的限制,未来会逐步解除:

  • 关键字参数语法。目前,从 Python 调用的 Mojo 函数只有在使用末尾的kwargs: StringDict[PythonObject]参数时才接受关键字参数;原生**kwargs语法支持将在未来加入。

  • Mojo 包依赖。依赖除 Mojo stdlib 之外其他包(如 Modular Community 包渠道中的包)的 Mojo 代码,目前只在手动构建 Mojo 扩展模块时受支持,因为 Mojo import hook 目前不支持为 Mojo 包依赖指定导入路径。

  • 属性(Properties)。计算属性的 getter 与 setter 目前不受支持。

  • 预期的类型转换。少数 Mojo 标准库类型通过实现ConvertibleFromPythontrait,可以直接从等价的 Python 内建对象类型构造;但许多 Mojo 标准库类型尚未实现该 trait,如有需要可能得编写手动转换逻辑。

参考资料

  • Mojo 手册章节原文:Calling Mojo from Python
  • 可运行示例目录:Mojo/docs/site/code/manual/python/mojo-from-python(含 mojo_module.mojo、main.py 与 BUILD.bazel)
  • stdlib 绑定实现:bindings.mojo(PythonModuleBuilder/PythonTypeBuilder
  • PythonObject实现:python_object.mojo
  • 转换 trait:conversions.mojo

【免费下载链接】mojoThe Modular Platform (includes MAX & Mojo)项目地址: https://gitcode.com/GitHub_Trending/mo/mojo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询