news 2026/9/14 14:25:37

mypyc 深入解析:将类型注解的 Python 编译为 CPython C 扩展(随 Flipper Zero 固件工具链捆绑)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
mypyc 深入解析:将类型注解的 Python 编译为 CPython C 扩展(随 Flipper Zero 固件工具链捆绑)

mypyc 深入解析:将类型注解的 Python 编译为 CPython C 扩展(随 Flipper Zero 固件工具链捆绑)

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

mypyc 是 mypy 项目自带的"Python 到 C 扩展"编译器:它读取带有标准类型注解(PEP 484)的 Python 模块,直接生成 C 代码并编译为 CPython C 扩展,从而消除解释器开销。本文以该包自带的 README.md 为主体,结合包内完整文档与源码结构,系统讲解 mypyc 的严格编译语义、环境要求、安装与编译流程、与 CPython 的语义差异、性能优化手段及其内部实现原理。读完你可以独立判断哪些模块值得用 mypyc 编译、如何编译、会遇到哪些限制,并理解其 IR 与代码生成管线。

在本文所在仓库中,mypyc 以预编译形式捆绑在固件构建工具链的 Python 3.11 环境中(toolchain/x86_64-linux/lib/python3.11/site-packages/mypyc/),其目录内绝大多数模块(如irbuildcodegenanalysis等子包)均为*.cpython-311-x86_64-linux-gnu.so已编译扩展——可以推断工具链环境本身正是这类"编译加速的 Python"的受益者之一。

mypyc 是什么:核心定位与严格语义

Mypyc 是一个将mypy 注解过的、静态类型的 Python 模块编译为CPython C 扩展的编译器。按 README.md 的说明,其当前首要目标是让 mypy 自身跑得更快——mypy 官方 wheel 就是用 mypyc 编译的,编译后的 mypy 比未编译时大约快 4 倍(这是项目文档声明的数据)。

Mypyc 编译的是一种基于 Python 语言变体的"严格语义"代码,这意味着(除其他事项外):

  • 大多数类型注解在运行时被强制执行,类型不匹配时抛出TypeError
  • 类被编译为扩展类(extension classes),没有__dict__,行为上非常接近(但不等同于)使用了__slots__
  • monkey patching 不生效
  • 实例属性在未定义时不会回退到类属性
  • 此外仍存在不少 bug 与不支持的特性。

编译后的模块可以 import 任意 Python 模块,也可以被其他 Python 模块调用。通常的做法是只编译包含性能瓶颈的模块。由于 mypyc 针对的是合法的 Python 代码,编译后的模块也可以直接作为普通解释型 Python 模块运行,因此所有 Python 开发工具和调试器都可以照常使用。

关于性能提升幅度,包内 introduction.rst 给出了更细的量化声明:

  • 已有类型注解的代码编译后通常快1.5 到 5 倍
  • 为 mypyc 调优过的代码可以快5 到 10 倍
  • mypyc 当前的目标是加速非数值计算类代码,例如服务器应用;mypyc 也被用来编译自身和 mypy。

支持平台与环境要求

README.md 对各平台给出了明确要求:

平台要求
macOSmacOS Sierra 或更高版本;需要 Xcode 命令行工具;建议使用 python.org 提供的 Python 3.5+(其他版本未测试)
Linux足够新的 C/C++ 构建环境;Python 3.5+
Windows已在 Windows 10 + MSVC 2017 上测试;Python 3.5+

包内 getting_started.rst 补充了更具体的构建依赖安装方式:

  • macOS:xcode-select --install安装 Xcode 命令行工具;
  • Linux(以 Ubuntu 18.04 为例):sudo apt install python3-dev,即需要 C 编译器以及 CPython 头文件和库;
  • Windows:从 Visual Studio 2022 Build Tools 安装对应架构的 MSVC C++ 构建工具和一个 Windows SDK。

贡献者快速上手:从源码跑通测试

README.md 给出了贡献者工作流(注意这是针对 mypy/mypyc 上游仓库的开发流程,本文仅作流程说明,不包含外部链接):

  1. 克隆 mypy 的 git 仓库,并cd进入;
  2. (推荐)创建虚拟环境:
    $ python3 -m venv <directory> $ source <directory>/bin/activate
  3. 安装依赖:
    $ python3 -m pip install -r test-requirements.txt
  4. 运行 mypyc 测试:
    $ pytest -q mypyc

dev-intro.md 对测试体系做了更深入的说明:大多数测试用例采用与 mypy 相同的.test格式,存放在mypyc/test-data/目录(本文档仓库中对应 test-data/),其中包括 74 个.test文件与若干.py.pyi夹具文件。CI 会额外用 mypyc 编译 mypy 并运行完整 mypy 测试套件,以捕捉本地测试发现不了的问题。

安装、编译与运行:一个可复现的完整示例

安装 mypyc

mypyc 随 mypy 发行版一起分发(需要 Python 3.8 及以上):

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

某些系统上使用python -m pip install -U 'mypy[mypyc]'

示例程序 fib.py

以经典的递归斐波那契微基准为例,保存为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函数带有-> int类型注解——没有注解的话,编译后的性能提升不会这么明显。

编译与对比运行

先用 CPython 以普通解释方式运行:

$ python3 fib.py 0.4125328063964844

然后用mypyc编译为二进制 C 扩展:

$ mypyc fib.py

这会在当前工作目录生成fib的 C 扩展,例如在 Linux 上可能叫fib.cpython-37m-x86_64-linux-gnu.so(在本文仓库的 Python 3.11 环境下则形如fib.cpython-311-x86_64-linux-gnu.so)。C 扩展不能直接作为程序运行,因此用python3 -c导入来运行:

$ python3 -c "import fib" 0.04097270965576172

文档示例中编译后大约快 10 倍。同时要注意:编译后fib.py中的__name__会是"fib"而不是"__main__"

mypyc命令行还接受大部分 mypy 命令行选项。想恢复解释版,直接删除生成的.so文件即可(Linux 示例):

$ rm fib.*.so

通过 setup.py 集成到项目

也可以用setup.py的方式编译,核心是mypycify(...)函数:

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

mypycify之外列出的 Python 文件不会被编译。随后可以打包 wheel:

$ python3 setup.py bdist_wheel

wheel 生成在dist/下;也可以原地编译(类似直接使用mypyc命令):

$ python3 setup.py build_ext --inplace

mypycify的参数列表中可以混入 mypy 命令行选项,例如用--disallow-untyped-defs强制所有函数都带类型注解:

setup( name='frobnicate', packages=['frobnicate'], ext_modules=mypycify([ '--disallow-untyped-defs', # 传入 mypy 标志 'frobnicate.py', ]), )

官方文档特别提示:不要轻易使用--check-untyped-defs去检查没有类型注解的函数,因为大量"已检查/未检查"代码之间的切换会显著拉低性能。

推荐开发工作流:解释模式为主、编译模式验证

getting_started.rst 给出的官方推荐工作流(mypy/mypyc 自身就是这样开发的):

  1. 开发阶段使用解释模式,获得快速的编辑-运行循环;
  2. 大量使用类型注解并用 mypy 静态检查,在注解覆盖良好的前提下,mypy 与测试可以提前发现绝大多数会破坏编译代码的错误;
  3. 实现完一个功能或修复后,编译项目并以编译模式再跑一遍测试,通常在注解覆盖良好时不会出问题;这一步可以在本地或 CI 中完成;
  4. 发布/部署编译版本,可选地保留一个解释版回退,用于 mypyc 不支持的平台。

配合 mypy 增量运行(尤其用 mypy daemon 时)通常只要几百毫秒,解释模式下的开发体验几乎和纯 Python 项目无异。

编译代码与 CPython 的行为差异

README.md 列出的严格语义,在 differences_from_python.rst 中被展开为一份详细的差异清单:

  • 运行方式:不能用python3 <module>.pypython3 -m <module>运行编译模块,要用python3 -c "import <module>"或写一个 wrapper 脚本;因此if __name__ == "__main__":在编译代码中不可依赖。
  • 类型错误会阻止编译:产生 mypy 类型错误的代码无法编译;# type: ignore偶尔可以绕过,但可能生成坏代码,被视为危险用法。
  • 运行时类型检查:非擦除类型(non-erased types)的注解会在运行时被检查。例如def twice(x: int) -> int,传twice(5)正常,传twice(2.2)twice("blah")会抛TypeError。带推断类型的值同样会被检查:编译代码调用socket.gethostname()时,mypyc 依据 typeshed 中的 stub(def gethostname() -> str)推断返回类型为str,若实际返回不兼容值(如None)会抛TypeErrorcast(str, x)在编译后等价于if not isinstance(y, str): raise TypeError(...),而解释模式下cast不做任何运行时检查。
  • 原生类:行为与普通 Python 类不同,详见下文。
  • 基本类型int使用非堆分配(unboxed)表示,代价是精确运行时类型信息丢失——first_int([True])输出1而不是Trueboolint子类,转成int时被转换);整数仍保持任意精度。定长元组同样被 unboxed,精确类型与同一性不保留,不能可靠地用is比较。
  • 早绑定(early binding):同一编译单元内的函数、类型、多数属性和方法引用在编译期确定目标,省略了运行时的命名空间查找;而非常数(non-final)的模块级变量仍使用晚绑定,性能关键代码应避免。
  • pickle 与拷贝:构造对象时会隐式调用__init__。若原生类不支持无参__init__,则无法 pickle/copy;可用mypy_extensions.mypyc_attr装饰器开启serializable标志。注意:开启序列化可能拖慢属性访问,且子类自动继承该标志。
  • monkey patching:编译函数的定义不可变,不能任意替换函数/方法(比如在测试里打 mock)。每个编译模块的 Python 命名空间本身是普通 dict、可以被修改,但编译代码一般不走这个命名空间,改动只对非编译代码可见。
  • 栈溢出:编译代码目前不检查栈溢出,失控递归可能导致程序不可恢复地崩溃(文档注明该限制未来会修复)。
  • Final 值:对Final声明的属性引用会被替换为编译期计算的常量值,例如MAX: Final = 100中两处MAX引用等效于字面量100,解释模式下这些引用反而更慢且可被修改。

尚不支持的 Python 特性

文档明确列出这些特性不能用于编译代码(或存在限制):

  • 嵌套类
  • 被 if 语句保护的(条件)函数或类定义
  • 原生类的部分dunder 方法__del____index____getattr____getattribute____setattr____delattr__
  • 生成器表达式(会被隐式替换为列表推导,语义并不总是等价;可用生成器函数或显式列表推导绕过);
  • 任意描述符(property、staticmethod、classmethod 受支持);
  • 多种内省手段:实例__annotations__通常不保留、inspect无法检查编译函数帧、inspect.ismethod不识别编译方法、inspect.signature会在编译函数上出错;
  • 性能分析与追踪钩子profilecProfiletrace不会触发编译函数的钩子;
  • 调试器:无法在编译函数中设置断点或用pdb单步,通常应改为在解释模式下调试。

原生类(Native Classes)详解

类在编译模块中默认为原生类,被编译为 C 扩展类,很多方面类似intstrlist等内建类型。native_classes.rst 说明了其关键约束:

  • 不可变命名空间:类型对象命名空间基本不可变——Cls.method1 = Cls.method2Cls.new_method = ...都是错误;只能给类定义体内(或基类中)声明过的属性赋值。未在类中声明的实例属性(如o.extra = 3)会报错。
  • 继承:只支持单继承(trait 类型除外)。可作为非原生基类的仅有:objectdict/Dict[k, v]BaseExceptionExceptionValueErrorIndexErrorLookupErrorUserWarningtyping.NamedTupleenum.Enum。默认不允许非原生类继承原生类、也不允许在定义原生类的编译单元之外继承它;可通过@mypyc_attr(allow_interpreted_subclasses=True)开启,代价是非原生子类的属性/方法访问变慢(走普通 Python 属性访问机制)。
  • 类变量:必须显式用ClassVar/ClassVar[<type>]声明;不能通过实例给类变量赋值(o.cv = 3报错);常量类变量可用Final声明。
  • 泛型原生类:类型变量在运行时被擦除,实例不记录类型参数值;运行时类型检查无法核对类型变量值,要等到"以类型变量类型读取值"时才检查。
  • 元类:多数元类不支持,可用abc.ABCMetatyping.GenericMeta;遇到不支持的元类时,mypyc会把该类编译成普通 Python 类
  • 类装饰器:多数装饰器太动态而不支持;可用mypy_extensions.traitmypy_extensions.mypyc_attrdataclasses.dataclass@attr.s(auto_attribs=True)。dataclass 与 attrs 类只有部分原生支持,效率不如纯原生类;遇到不支持的类装饰器时同样回退为普通 Python 类。
  • 删除属性:默认属性不可删除;用类体内的__deletable__ = ['x', 'y']显式允许,且必须用仅含字符串字面量的列表/元组表达式就地初始化(用变量赋值是错误,写在类体外也是错误)。
  • 原生类实例通常没有__dict__

类型注解与性能:哪些类型"值钱"

using_type_annotations.rst 给出了决定编译收益的关键分类:

基本类型(primitive types)——这些类型有大量高效的原生实现,是性能提升的核心:inti64i32i16u8floatboolstrList[T]Dict[K, V]Set[T]、变长Tuple[T, ...]None。所有 Python 操作都可用,但其中的"原生操作"有定制的优化实现。

原生类——构造、属性访问、方法调用等最常见操作都被优化。

元组类型——定长元组(如Tuple[int, str])作为变量、参数或返回值时是值类型,分配在机器栈或 CPU 寄存器上;存入 Python 容器或传入非原生代码时会被 boxed 成普通 Python tuple。

联合类型 / Optional——含基本类型、原生类、trait 的联合类型同样高效;Optional[int]相当高效,但值总是被 boxed,普通int(unboxed 表示)通常更快。

trait 类型——用@mypy_extensions.trait定义,为原生类提供有限的多继承形式;trait 必须放在基类列表末尾;通过 trait 类型访问方法/属性略慢于原生类,但远快于普通 Python 类。

擦除类型(erased types)——普通 Python 类、非基本内建类型、Callable、类型变量、Any、协议类型等没有定制操作,运行时等价于Any。但用好擦除类型仍有收益:例如f: Callable[[], int]调用本身慢,但返回值的推断类型是基本类型int,后续n += 1可以用快速操作。

值类型与堆类型boolfloatNone、原生整数类型和定长元组是值类型;int是混合体——常规大小(64 位平台 63 位以内)走值表示,更大时走堆表示(同 CPython)。由此产生的语义包括:整数/浮点/元组的对象同一性不保留,应使用==而非isbool赋给int类型变量时隐式转换为对应整数(这是 mypyc 中唯一的隐式类型转换);编译代码中禁止把int赋给float变量(即使初始化也不行),必须显式float(n)

原生整数类型i64/i32/i16/u8,从mypy_extensions导入):固定位宽、无溢出检查,比任意精度int更快、更省内存。int与原生整数之间可双向隐式转换;不同原生整数类型之间必须显式转换(窄化转换截断且无运行时溢出检查);混用int与原生整数的二元运算会把int操作数隐式强制转为原生整数("粘性")。注意:在解释模式下这些类型等价于int,声明它们不产生任何效果

编译单元:跨模块调用的代价

compilation_units.rst 解释了"编译单元"概念:一次mypyc调用编译的一组模块构成一个编译单元,单元内部使用早绑定;多次调用产生多个独立编译单元,跨单元引用回退到晚绑定,所有调用走较慢的 Python 调用约定(参数与返回值都要 boxed)。因此,为获得最大性能,应最小化跨编译单元交互——最简单的做法是把整个程序作为一个编译单元一次编译。

性能优化实战技巧

performance_tips_and_tricks.rst 总结了实战要点:

  • 先剖析:mypyc 只加速你编译的代码。若 40% 时间花在编译代码之外,即使编译代码快 100 倍,总体也只快 2.5 倍。可用time.time()粗测或cProfile细测(对非编译代码有效)。
  • 避开慢库:若时间集中在少数库功能上,考虑用带注解的 Python 重实现并编译,或换用更高效的库。
  • 补注解:至少给性能关键函数/类加注解;给被调用但未编译的代码加注解有助于类型推断;外部库应选用 stub 覆盖良好的。若不想给外部代码写 stub,可对变量做显式注解,例如items: List[Tuple[int, str]] = acme.get_items(),否则items会推断为Any,后续操作全部变慢。
  • 避开慢特性:未受支持的类装饰器/元类、对解释型库的重度依赖、普通 Python 类、非特殊处理的装饰函数、嵌套函数、跨编译单元调用、*args/**kwargs、生成器函数、可调用值(未走早绑定)等。嵌套函数可用模块级函数或原生类方法替代;可调用值可用带单个call(...)方法的原生类实例替代。
  • 使用快速原生特性:直接调用同编译单元内的编译函数/原生类方法、多数整数与float运算、布尔、原生 list 操作(索引、append、列表推导)、while 循环、对 range/list 的 for 循环(含enumerate/zip)、读 dict 项、针对原生类与基本类型(及其联合)的isinstance、局部变量访问、原生类属性访问、Final 模块级属性访问、字符串相等比较都非常快。注意在 CPython 里"把方法缓存到局部变量"(如append = a.append)的优化在编译代码中会变慢,因为它破坏了早绑定。
  • 调整 GC:编译不加速循环垃圾回收,可用gc.set_threshold(150000)让 GC 少跑(在主要计算之前调用)。
  • 快速解释器退出:若进程清理耗时,批量任务可调用os._exit(code)跳过清理,但必须确保流已 flush、数据已落盘,否则可能丢数据。
  • 聪明的取舍:与其抠低级细节,不如优先移除阻止关键类被编译成原生类的元类——一次改动就能让大量方法调用和属性访问整体提速。

内部实现:IR、代码生成与运行时支持库

dev-intro.md 提供了源码级剖析,且其描述的模块结构在本文仓库的 mypyc/ 中一一对应:

编译管线(passes)

  1. 用 mypy 做类型检查与推断,产出 mypy AST 和类型映射;
  2. 把 mypy AST 翻译为 mypyc 专属的中间表示(IR)——IR 定义在 ir/(ops.py中的OpBasicBlockrtypes.py中的RType,以及func_ir.pyclass_ir.py),翻译逻辑在 irbuild/(入口为main.py);
  3. 插入未初始化变量检查(transform/);
  4. 插入异常处理;
  5. 插入显式引用计数增减操作;
  6. 把 IR 翻译成 C(codegen/);
  7. 用 C 编译器编译生成的 C 代码(mypyc.build)。

值表示int使用带标签指针(CPyTagged,见 lib-rt/CPy.h 与 lib-rt/int_ops.c),boolchar,元组用 C 结构体;其余对象用PyObject *

生成 C 的结构:每个函数编译为"原生函数 + wrapper 函数"两个 C 函数——原生函数接收固定数量、类型正确的 C 参数,不做类型检查;wrapper 遵循 Python C API 调用约定,负责处理参数、类型检查、拆箱并调用原生函数,返回值再 box 回 Python 对象。编译函数之间的调用不走模块命名空间,直接调用目标原生 C 函数。生成的 C 代码存放在build/__native.c,原生函数以CPyDef_为前缀、供解释代码调用的 wrapper 以CPyPy_为前缀。运行时辅助实现集中在mypyc/lib-rt/下(CPy.hint_ops.clist_ops.cdict_ops.cstr_ops.ctuple_ops.c等),对应的 C 单元测试在 lib-rt/test_capi.cc(基于仓库内捆绑的 googletest)。

原生操作的定义mypyc.primitives中以数据驱动方式声明针对listint等基本类型的特化操作,编译时通过 AST 匹配自动挑选最合适的实现;新增基本类型需要更新mypyc.ir.rtypesirbuild.mapper.Mapper.type_to_rtype()以及codegen.emit中的emit_box/emit_unbox/emit_inc_ref等系列函数。

开发状态与路线图

README.md 记录了里程碑式路线图,大部分已标记完成:

  1. 支持"小而有用"的 Python 子集,聚焦单模块编译,程序其余部分保持解释执行(已完成);
  2. 支持把多个模块作为单一编译单元编译/动态链接(已完成);
  3. mypyc 能够编译 mypy 自身(已完成);
  4. 优化若干关键性能瓶颈(已完成);
  5. 对不支持的 Python 特性生成有用的错误信息,而非崩溃或生成坏代码(部分完成);
  6. 发布包含编译版 mypy 的 mypy 版本(已完成);
  7. 更多特性/兼容性工作(与 Python 100% 兼容明确是反目标,但比现状更多是好事;支持编译 Black 已完成;更多优化,尤其是代码体积缩减);
  8. 后续待定。

dev-intro.md 同时提醒:mypyc 目前仍是 alpha 软件,仅建议在充分测试、且愿意贡献修复或绕行已知问题时用于生产;当前也不检测栈溢出、不在编译代码中处理 Ctrl-C(两项都计划未来修复)。

在本文仓库中的位置与用途

本文所述 mypyc 以完整包的形式捆绑在仓库工具链中:toolchain/x86_64-linux/lib/python3.11/site-packages/mypyc/,与固件构建工具链(fbtsite_scons等)使用同一套 Python 3.11 环境。包内除了 README.md 和 doc/ 下的全套 RST 文档(introductiongetting_starteddifferences_from_pythonnative_classesusing_type_annotationsperformance_tips_and_trickscompilation_unitsdev-introfuture等),还包含已编译的*.cpython-311-x86_64-linux-gnu.so模块、test-data/测试数据与lib-rt/C 运行时源码。这意味着如果你在固件开发之外的工具链脚本中遇到性能瓶颈,完全可以在该 Python 3.11 环境内按本文流程安装并使用 mypyc,对带类型注解的工具脚本做同样的"注解 → 编译 → 加速"改造;需要注意的是 mypyc 只加速被编译的模块,且要遵循上文提到的严格语义约束。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 14:24:00

表情识别毕设实战:从CNN模型训练到实时摄像头部署

简介&#xff1a;面向计算机专业学生&#xff0c;这是一份基于Pytorch构建的卷积神经网络面部表情识别毕业设计项目&#xff0c;评审得分高达98分&#xff0c;且已严格调试可运行。资源包内含2000个文件&#xff0c;其中有1992张JPG表情图像、6个Python源码、1个CSV标注文件及1…

作者头像 李华
网站建设 2026/9/14 14:23:55

SAR点目标仿真全链路:回波生成、距离徙动校正与指标验证

简介&#xff1a;合成孔径雷达&#xff08;SAR&#xff09;点目标仿真与RD成像算法是雷达信号处理教学中的经典内容。这份MATLAB工程资料面向正在学习SAR成像原理、需要完成点目标仿真实验的本科生、研究生及相关领域工程师&#xff0c;能够帮助读者在真实代码层面理解距离压缩…

作者头像 李华
网站建设 2026/9/14 14:21:46

uni-app社区团购APP三端落地实战:蓝牙核销与多端适配

简介&#xff1a;这是一份基于uni-app开发的社区团购类APP源码模板&#xff0c;面向前端开发者与跨端应用学习者&#xff0c;聚焦社区生鲜电商场景&#xff0c;提供开箱即用的购物流程、拼团机制与多端适配能力。资源包为ZIP格式&#xff0c;大小935KB&#xff0c;虽未提供具体…

作者头像 李华