ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

Mypyc 入门指南:将带类型注解的 Python 模块编译为 C 扩展 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来生成快速代码编译后的产物是以.soLinux/macOS或.pydWindows为后缀的原生扩展模块。从源码结构看mypyc 本身就是一个完整的编译工具链当前仓库的 mypyc 包目录 下包含analysis/类型与数据流分析、codegen/C 代码生成、ir/与irbuild/中间表示与构建、transform/IR 变换、lib-rt/运行时库等模块说明其内部遵循分析 → 中间表示 → 代码生成的经典编译器流水线。在打包进当前仓库 toolchain 时mypyc 自身的多个核心模块如__init__、common、options、subtype等也已被编译成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 --installLinux需要 C 编译器以及 CPython 头文件与库。以 Ubuntu 18.04 为例$ sudo apt install python3-devWindows从 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.pyimport 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_LEVELC 编译优化级别默认3对应 GCC/Clang 的-O3MYPYC_DEBUG_LEVEL调试信息级别默认1对应 GCC/Clang 的-g1。对应的 build.py 中mypycify()的opt_level与debug_level参数默认值正是3与1在 Windows/MSVC 上这两个值会被映射为/O2与/DEBUG:FASTLINKopt_level0时优化会关闭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( namemylib, packages[mylib], ext_modulesmypycify([ mylib/__init__.py, mylib/mod.py, ]), )mypycify(...)用于指定哪些文件要被 mypyc 编译setup.py中也可以包含不在mypycify(...)内的其他 Python 文件这些文件将保持解释执行、不会被编译。构建 wheel.whl文件$ python3 setup.py bdist_wheelwheel 会生成在dist/目录下。也可以在当前目录就地编译 C 扩展效果类似于直接用mypyc编译模块$ python3 setup.py build_ext --inplace大部分 mypy 命令行选项都可以放进传给mypycify()的参数列表中。例如用--disallow-untyped-defs强制要求所有函数都带类型注解... setup( namefrobnicate, packages[frobnicate], ext_modulesmypycify([ --disallow-untyped-defs, # Pass a mypy flag frobnicate.py, ]), )一个值得注意的坑有人会想用--check-untyped-defs去类型检查那些没有注解的函数。但文档明确警告这样做可能降低性能因为类型检查代码与未检查代码之间会产生大量边界转换type-checked / unchecked 代码之间的往返装箱与检查开销。推荐工作流解释模式开发编译模式发布最省事的做法是每次改完代码就编译但当代码量增大后这很快会变得繁琐。文档推荐与 mypy/mypyc 自身开发时一致的工作流核心思想是大多数开发、测试和调试都在解释模式下进行开发阶段使用解释模式获得快速的编辑-运行循环大量使用类型注解并用 mypy 在开发期间做类型检查。只要注解覆盖良好mypy 和测试就能提前发现绝大多数会导致编译产物出错的错误运行 mypy 通常很快实现完一个功能或修复后编译项目并在编译模式下再跑一遍测试。注解覆盖良好时这一步通常不会出问题可以在本地执行也可以放进持续集成CI任务有 CI 的话本地编译频率可以很低发布或部署编译版本。可选地为 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/zip的for循环、读取字典项、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.rstmypyc 与标准 Python 在运行期行为上的差异performance_tips_and_tricks.rst进阶性能调优技巧introduction.rstmypyc 的设计动机、与 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),仅供参考
返回列表