Mypyc 入门指南:将带类型注解的 Python 模块编译为 C 扩展
2026/9/14 20:10:17 网站建设 项目流程

Mypyc 入门指南:将带类型注解的 Python 模块编译为 C 扩展

【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware

导读

本文基于 mypyc 官方文档 getting_started.rst 编写,系统讲解 mypyc 的完整入门流程:环境准备、安装、第一个编译示例、setup.py 集成方式与推荐开发工作流。读完本文,你将掌握如何把带类型注解的 Python 模块编译成 CPython C 扩展,在保持 Python 开发体验的同时获得显著性能提升。文中的源码级佐证均来自当前仓库自带 toolchain 中随 mypy 一起分发的 mypyc 包(mypyc/README.md、introduction.rst),便于你在本地继续深入。

mypyc 是什么:面向 C 扩展的 Python 编译器

Mypyc 是一个把mypy 注解过的、静态类型的 Python 模块编译为CPython C 扩展的编译器。它使用标准 Python 类型注解(type hints)来生成快速代码,编译后的产物是以.so(Linux/macOS)或.pyd(Windows)为后缀的原生扩展模块。

从源码结构看,mypyc 本身就是一个完整的编译工具链:当前仓库的 mypyc 包目录 下包含analysis/(类型与数据流分析)、codegen/(C 代码生成)、ir/irbuild/(中间表示与构建)、transform/(IR 变换)、lib-rt/(运行时库)等模块,说明其内部遵循"分析 → 中间表示 → 代码生成"的经典编译器流水线。在打包进当前仓库 toolchain 时,mypyc 自身的多个核心模块(如__init__commonoptionssubtype等)也已被编译成cpython-311-x86_64-linux-gnu.so形式的 C 扩展,属于"用 mypyc 编译自身"的典型实践。

核心特性:严格、渐进类型的 Python 变体

根据 mypyc/README.md 的说明,mypyc 编译的是采用 "strict"(严格)语义的 Python 语言变体,这意味着:

  • 大多数类型注解在运行时被强制执行,类型不匹配时抛出TypeError
  • 类被编译为扩展类(extension class),不再依赖__dict__,行为上很像使用__slots__
  • 猴子补丁(monkey patching)不生效
  • 实例属性在未定义时不会回退到类属性

同时,编译后的模块可以导入任意 Python 模块,也可以被其他 Python 模块导入。典型用法是只编译包含性能瓶颈的模块,其余部分保持解释执行。因为 mypyc 面向的是合法 Python 代码,编译产物同样可以作为普通 Python 模块运行,所有 Python 开发工具与调试器都可以正常使用。

为什么值得使用

introduction.rst 中给出了选择 mypyc 的理由,核心包括:

  • 上手容易:编译代码的观感与普通 Python 代码几乎一致,不需要cpdef之类的非标准语法;
  • 类型表达力强:完整支持typing模块中的大部分特性,包括元组类型、联合类型、泛型、可选类型,并有由 mypy 提供的强大局部类型推断;
  • 生态兼容:运行在 CPython 之上,可以照常使用 pip 安装的第三方库(包括 C 扩展);
  • 启动速度快:采用提前编译(ahead-of-time compilation),编译不拖慢程序启动,这是相对 JIT 编译器的常见优势;
  • 运行期类型安全:运行值会按注解校验,避免段错误与内存破坏,异常的类型违规会被视为 mypyc 的 bug;
  • 可迁移性:已有类型注解的代码往往只需少量修改即可编译。

introduction.rst 同时指出:已有类型注解的代码编译后通常快 1.5 到 5 倍;为 mypyc 调优过的代码可快 5 到 10 倍。作为参照,mypy 默认发行版的轮子就是由 mypyc 编译的,mypyc/README.md 提到编译后的 mypy 比解释执行快约 4 倍。此外,文档明确说明 mypyc 目前是 alpha 软件,用于生产需要充分测试并愿意处理遇到的问题。mypyc 与 Cython 的不同之处在于:无需非标准语法即可获得高性能、对typing模块特性有一等支持、与 mypy 深度集成做静态检查,但它不直接支持对接 C 库或加速数值计算代码。

前置条件:不同平台的 C 扩展开发环境

编译 C 扩展需要一个可用的 Python C 扩展开发环境,具体安装方式随操作系统而异:

  • macOS:安装 Xcode 命令行工具:

    $ xcode-select --install
  • Linux:需要 C 编译器以及 CPython 头文件与库。以 Ubuntu 18.04 为例:

    $ sudo apt install python3-dev
  • Windows:从 "Build Tools for Visual Studio 2022" 安装适用于本机架构的 MSVC C++ 构建工具和一个 Windows SDK(建议使用最新版本)。

需要说明的是,本指南适用于任意安装 mypy 的 Python 环境;而在当前仓库的 toolchain/x86_64-linux 中已经自带了 Python 3.11 及 mypyc(其扩展文件名为cpython-311-x86_64-linux-gnu.so,可见该环境面向 CPython 3.11),如果你只想在本仓库自带的 toolchain 内体验 mypyc,可以直接使用其中预置的解释器,无需额外安装系统级依赖。

安装:mypyc 随 mypy 一起分发

Mypyc 作为 mypy 发行版的一部分随 mypy 一起安装。要求Python 3.8 或更高版本

$ python3 -m pip install -U 'mypy[mypyc]'

某些系统上需要使用:

$ python -m pip install -U 'mypy[mypyc]'

注意这里必须安装带[mypyc]extra 的 mypy,它才会把 mypyc 编译器及其运行时依赖一并装上。

第一个编译示例:递归 Fibonacci

文档用一个经典的微基准——递归斐波那契——来演示整个流程。将下面代码保存为fib.py

import time def fib(n: int) -> int: if n <= 1: return n else: return fib(n - 2) + fib(n - 1) t0 = time.time() fib(32) print(time.time() - t0)

关键点:必须给fib函数加上类型注解。文档明确指出,如果没有注解,编译后的性能提升会大打折扣——因为未注解的部分会被当作动态类型处理,无法生成类型特化的快速代码。

如果你对类型注解或 mypy 还不熟悉,introduction.rst 和 using_type_annotations.rst 提供了相关背景。mypyc 正是借助 mypy 完成类型检查与类型推断的,对 mypy 有一定了解会很有帮助。

编译与运行:从 0.41s 到 0.04s

先用 CPython 以解释模式运行:

$ python3 fib.py 0.4125328063964844

上面是文档作者机器上的结果(约 0.41 秒)。随后运行 mypyc 把程序编译为二进制 C 扩展:

$ mypyc fib.py

这条命令会在当前工作目录下生成一个针对fib的 C 扩展。例如在 Linux 上,生成的文件可能叫fib.cpython-37m-x86_64-linux-gnu.so(具体后缀取决于你的 Python 版本与平台;在当前仓库的 CPython 3.11 Linux 环境中会形如fib.cpython-311-x86_64-linux-gnu.so)。

由于 C 扩展不能像脚本一样直接执行,需要通过python3 -c以导入模块的方式运行编译产物:

$ python3 -c "import fib" 0.04097270965576172

在文档作者的演示中,编译后程序约快10 倍。需要留意一个行为差异:编译后fib.py内的__name__现在是"fib"而不是"__main__",因此依赖if __name__ == "__main__":的入口代码在编译后不会再作为程序入口执行。

mypyc 命令行本身是一个薄封装:从main.py 的源码可以看到,mypyc命令实际上是在build/目录下生成一个基于mypycify()setup.py,然后调用setup.py build_ext --inplace完成就地编译。该实现还支持两个环境变量:

  • MYPYC_OPT_LEVEL:C 编译优化级别,默认"3"(对应 GCC/Clang 的-O3);
  • MYPYC_DEBUG_LEVEL:调试信息级别,默认"1"(对应 GCC/Clang 的-g1)。

对应的 build.py 中,mypycify()opt_leveldebug_level参数默认值正是"3""1";在 Windows/MSVC 上这两个值会被映射为/O2/DEBUG:FASTLINKopt_level=0时优化会关闭,debug_level"2"/"3"时调试信息升为FULL)。此外,大部分 mypy 命令行选项都可以原样传给mypyc,用于控制类型检查行为。

清理编译产物:回到解释模式

如果想回到解释执行版本,手动删除生成的 C 扩展即可(以下命令适用于 Linux):

$ rm fib.*.so

删除后,import fib又会加载同目录下的fib.py源码(若存在),回到解释执行。

通过 setup.py 集成到正式项目

除了直接使用mypyc命令,更正式的做法是在项目构建脚本里通过setup.py调用mypycify()编译指定模块。下面是一个完整的setup.py示例:

from setuptools import setup from mypyc.build import mypycify setup( name='mylib', packages=['mylib'], ext_modules=mypycify([ 'mylib/__init__.py', 'mylib/mod.py', ]), )

mypycify(...)用于指定哪些文件要被 mypyc 编译;setup.py中也可以包含不在mypycify(...)内的其他 Python 文件,这些文件将保持解释执行、不会被编译。

构建 wheel(.whl)文件:

$ python3 setup.py bdist_wheel

wheel 会生成在dist/目录下。

也可以在当前目录就地编译 C 扩展(效果类似于直接用mypyc编译模块):

$ python3 setup.py build_ext --inplace

大部分 mypy 命令行选项都可以放进传给mypycify()的参数列表中。例如,用--disallow-untyped-defs强制要求所有函数都带类型注解:

... setup( name='frobnicate', packages=['frobnicate'], ext_modules=mypycify([ '--disallow-untyped-defs', # Pass a mypy flag 'frobnicate.py', ]), )

一个值得注意的坑:有人会想用--check-untyped-defs去类型检查那些没有注解的函数。但文档明确警告,这样做可能降低性能,因为类型检查代码与未检查代码之间会产生大量边界转换(type-checked / unchecked 代码之间的往返装箱与检查开销)。

推荐工作流:解释模式开发,编译模式发布

最省事的做法是每次改完代码就编译,但当代码量增大后这很快会变得繁琐。文档推荐与 mypy/mypyc 自身开发时一致的工作流,核心思想是大多数开发、测试和调试都在解释模式下进行

  1. 开发阶段使用解释模式,获得快速的"编辑-运行"循环;
  2. 大量使用类型注解,并用 mypy 在开发期间做类型检查。只要注解覆盖良好,mypy 和测试就能提前发现绝大多数会导致编译产物出错的错误(运行 mypy 通常很快);
  3. 实现完一个功能或修复后,编译项目并在编译模式下再跑一遍测试。注解覆盖良好时,这一步通常不会出问题,可以在本地执行,也可以放进持续集成(CI)任务;有 CI 的话,本地编译频率可以很低;
  4. 发布或部署编译版本。可选地,为 mypyc 不支持的平台保留一份可解释执行的回退版本。

这个工作流只对典型 Python 工作流做了微小改动:绝大部分开发、测试、调试都在解释模式完成;增量式 mypy 检查(尤其是使用 mypy daemon 时)通常只需要几百毫秒。它同时呼应了 introduction.rst 中"编译整个代码库"的用法:开发时解释执行,发布时把非测试代码全部编译。

编译单元:影响性能的关键概念

理解 mypyc 性能,必须理解编译单元(compilation unit)。根据 compilation_units.rst:

  • 一次mypyc运行编译的一组模块共同构成一个编译单元,单元内部的引用会使用早期绑定(early binding),即编译期解析函数调用和名字引用,避免动态命名空间查找;
  • 如果多次运行 mypyc 编译多组模块,每次运行都会产生一个新的编译单元;跨编译单元的引用会回退到晚期绑定(通过 Python 命名空间字典查名字),所有调用都会走较慢的 Python 调用约定,参数与返回值会被装箱(boxed)再在调用方可能重新拆箱。

因此,为了最大化性能,应尽量减小跨编译单元的交互,最简单的方式就是把整个程序作为一个编译单元一次编译。

性能优化要点

performance_tips_and_tricks.rst 给出了一系列实战建议,以下是核心几条:

  • 先剖析再优化:mypyc 只加速你编译的代码。如果 40% 的时间花在编译代码之外,即使编译部分快 100 倍,总体也只能快 2.5 倍。可以使用time.time()埋点,或使用 stdlib 的profile/cProfile(后者只对非编译代码效果好);
  • 避开慢库:若剖析显示时间花在 stdlib 或第三方库,可以尝试用带注解的 Python 重写其中的热点,或换用更高效的替代库;
  • 补全注解:为性能关键的函数和类加注解,也为被其调用的代码加注解(即使不编译),这有助于 mypy 在编译代码中推断出更好的类型;对没有注解的第三方函数,可以显式注解接收结果的变量(例如items: List[Tuple[int, str]] = acme.get_items()),否则类型会是Any,后续操作会退化为较慢的通用实现;
  • 避免慢特性:如编译代码中使用不受支持的类装饰器/元类、依赖解释型 Python 库、使用普通 Python 类(原生类快得多)、调用装饰过的函数或嵌套函数、跨编译单元调用、使用*args/**kwargs、生成器函数、可调用对象等。嵌套函数可替换为模块级函数或原生类方法;可调用值可替换为只有一个call(...)方法的原生类实例;
  • 善用快特性:同一编译单元内的编译函数与原生类方法调用、大量整数/浮点运算、布尔运算、原生列表操作(索引、append、列表推导)、while循环、对range/列表/enumerate/zipfor循环、读取字典项、isinstance()检查、访问局部变量与原生类属性、比较字符串相等,都相对很快。反直觉的是:在 CPython 中把方法缓存到局部变量的优化(append = a.append)在编译代码中反而会变慢(破坏早期绑定);用字典替代类实例在编译代码中也往往是负优化;
  • 调整垃圾回收:编译不加速循环引用收集,若 GC 占比变大可用gc.set_threshold(150000)降低 GC 频率;
  • 快速退出:批量任务或命令行工具可在确认所有流已 flush、清理完毕的前提下调用os._exit(code)跳过正常关闭流程(有数据丢失风险,需谨慎)。

下一步学习路径

如果仅仅加注解并编译已经带来不错的收益,那可以到此为止;如果收益不明显、代码在 mypyc 下有问题,或者想为最大性能调优,建议继续按顺序阅读本仓库 mypyc 文档包(toolchain/x86_64-linux/lib/python3.11/site-packages/mypyc/doc/)中的下列主题:

  • using_type_annotations.rst:如何用好类型注解,让编译器获得更多信息;
  • native_classes.rst:原生类(编译为扩展类的 Python 类)的写法与限制;
  • differences_from_python.rst:mypyc 与标准 Python 在运行期行为上的差异;
  • performance_tips_and_tricks.rst:进阶性能调优技巧;
  • introduction.rst:mypyc 的设计动机、与 Cython 的对比及内部工作原理;
  • compilation_units.rst:编译单元与早期绑定机制详解。

同时,mypyc 包目录下还带有针对int/float/str/list/dict/set/tuple/bool等各类型原生操作的参考文档(如 int_operations.rst、list_operations.rst),在编写热点代码时可随时查阅哪些操作是"原生快路径"。

【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware

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

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

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

立即咨询