ARTICLE DETAIL

资讯详情

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

Mypyc 原生类(Native Classes)深度解析:编译期类语义、继承约束与性能权衡

Mypyc 原生类(Native Classes)深度解析:编译期类语义、继承约束与性能权衡 Mypyc 原生类Native Classes深度解析编译期类语义、继承约束与性能权衡【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware导读本文以 mypyc 官方文档 native_classes.rst 为主体系统讲解 mypyc 将 Python 类编译为 C 扩展类即原生类后所引入的语义差异不可变命名空间、受限的继承体系、显式类变量、类型变量擦除、元类与装饰器约束、属性删除控制等。该文档随 Flipper Zero 固件仓库内置的 Python 3.11 工具链一并分发位于toolchain/x86_64-linux/lib/python3.11/site-packages/mypyc/目录适用于所有希望在保持 Python 开发体验的同时把性能关键模块编译为 C 扩展的开发者。读完本文你将掌握原生类与普通 Python 类之间的全部关键差异知道哪些代码可以安全编译、哪些写法必须调整并能依据仓库源码理解每个约束背后的实现动机。什么是原生类在 mypyc 编译的模块中默认情况下所有类都是原生类native class。mypyc 会把这些类编译为 C 扩展类——其存在形态与 CPython 内置类型如int、str、list更为接近而非普通的 Python 类。这一设计源于 mypyc 的核心编译策略。根据同一文档目录下的 introduction.rstmypyc 将类型注解的 Python 模块**提前编译ahead-of-time compilation**为原生代码并使用以下技术提速编译后的类使用vtable虚方法表实现快速方法调用与属性访问编译函数、类以及声明为Final的属性被视为不可变从而启用早绑定early binding省去大量动态命名空间查找整数与布尔值采用内存高效的**非装箱unboxed**表示。原生类正是这套优化在类这个维度上的落地一旦类被编译为 C 扩展类其内部结构在运行时就是固定的编译器可以安全地静态化方法分派与属性读写。代价是原生类失去了普通 Python 类的一部分动态能力本文后续各节将逐一说明这些差异。值得注意的是mypyc 自身以及 mypy正是用 mypyc 编译的原生类语义在实践中经过了大量真实代码的检验。不可变命名空间类型对象是半只读的原生类的类型对象命名空间基本不可变但有一个例外类定义体内声明的类变量仍可被赋值。看下面的例子class Cls: def method1(self) - None: print(method1) def method2(self) - None: print(method2) Cls.method1 Cls.method2 # Error Cls.new_method Cls.method2 # Error在普通 Python 中Cls.method1 Cls.method2是合法且常见的动态替换方法实现但在原生类中这会导致编译错误。原因在于编译时 mypyc 已经把所有方法调用点静态绑定到了具体实现早绑定如果允许运行时随意替换方法编译期做出的分派假设就会失效。与命名空间约束配套的是属性集的静态固化只有类定义体内或基类中声明的属性可以被赋值行为类似于显式使用__slots__的类class Cls: x: int def __init__(self, y: int) - None: self.x 0 self.y y def method(self) - None: self.z x o Cls(0) print(o.x, o.y) # OK o.z y # OK o.extra 3 # Error: no attribute extra这里o.extra 3会报错因为extra既未在类体中声明也未在__init__或method中被赋值过。mypyc 正是通过追踪类体内所有被赋值的属性来建立每个原生类的固定属性表从而在编译期为属性访问生成直接的内存偏移/槽位访问而不是每次走字典查找。这一机制与 Python 标准库中的__slots__思路一致用属性集合的静态化换取内存与速度。区别在于原生类是 mypyc 编译器默认强制这一行为的而普通 Python 类需要开发者主动声明__slots__。继承规则仅支持单继承与受限基类基类白名单原生类只支持单继承特例是 trait 类型mypy_extensions.trait装饰器定义详见下文类装饰器一节。并且大多数非原生类不能作为原生类的基类。可以被用作原生类基类的非原生类只有以下白名单基类说明object一切类的根dict及Dict[k, v]字典类型BaseException异常基类体系Exception常规异常ValueError数值类异常IndexError索引类异常LookupError查找类异常UserWarning用户警告typing.NamedTuple命名元组enum.Enum枚举这些类型在 CPython 内部都是结构相对简单的内置类型其内存布局与调用约定是可预测的因此可以安全地作为编译期静态分派的起点。解释器子类的两种默认限制默认情况下还有两个方向上的限制非原生类不能继承原生类不能在定义某个原生类的编译单元之外继承它。也就是说默认模型是一个原生类只能在同一编译单元内部被继承且其子类也必须是原生类。这是为了保证子类与父类共享同一套静态化的属性表与 vtable 布局。用 mypyc_attr 打开扩展如果你确实需要更宽松的继承例如供解释执行的代码或其它编译单元继承可以借助mypy_extensions.mypyc_attr装饰器显式开启from mypy_extensions import mypyc_attr mypyc_attr(allow_interpreted_subclassesTrue) class Cls: ...设置allow_interpreted_subclassesTrue之后原生类实例本身的性能只受轻微影响代价是访问非原生子类或其它编译单元中的子类的方法与属性时会明显变慢——因为这些访问必须回退到普通的 Python 属性访问机制动态命名空间查找而无法使用编译期的快速路径。使用mypyc_attr前需要安装mypy-extensions包pip install --upgrade mypy-extensions从仓库源码看mypy_extensions.py 中mypyc_attr的实现只是一个返回原类的恒等装饰器def mypyc_attr(*attrs, **kwattrs): return lambda x: x这说明该类装饰器仅在 mypyc 编译期被解析运行时它什么都不做——所有语义如allow_interpreted_subclasses、serializable等开关都由编译器在生成 C 代码时读取普通解释模式下它只是传递原类。这是理解 mypyc 工具链编译期指令类 API 的关键装饰器源码是给 mypy 类型检查器与 mypyc 编译器看的元数据而不是运行时行为。类变量必须显式声明实例不可赋值原生类中的类变量必须显式声明语法为attr: ClassVar或attr: ClassVar[type]并且不能通过实例给类变量赋值from typing import ClassVar class Cls: cv: ClassVar 0 Cls.cv 2 # OK o Cls() print(o.cv) # OK (2) o.cv 3 # Error!ClassVar是typing模块中专门标记这是类变量而非实例变量的注解。在原生类中它不仅仅是类型提示而是编译器区分类体属性表与实例属性表的依据只有ClassVar声明的属性才会进入类型对象层面普通注解的属性则进入实例属性表。o.cv 3之所以报错是因为编译器把cv归类为类级变量实例上的赋值不合法。小贴士对于不会变化的常量类变量推荐用typing.Final或typing.Final[type]声明。被Final标记的属性在编译时会启用早绑定——所有对该属性的引用都被编译期常量直接替换不再产生任何命名空间查找这一点在 differences_from_python.rst 的 Final values 一节有详细阐述。泛型原生类类型变量在运行时被擦除原生类可以是泛型类。关键语义是类型变量在运行时被擦除实例不会保存类型变量值。因此编译后的代码无法在运行时类型检查中验证类型变量值——这些检查被推迟到读取一个类型为类型变量的值时执行from typing import TypeVar, Generic, cast T TypeVar(T) class Box(Generic[T]): def __init__(self, item: T) - None: self.item item x Box(1) # Box[int] y cast(Box[str], x) # OK (type variable value not checked) y.item # Runtime error: item is int, but str expected注意这里的行为链条Box(1)在类型上被推断为Box[int]cast(Box[str], x)不会报错因为Box实例本身不记录T的具体值编译器无从检查真正出错的是读取y.item的时刻——运行时发现实际值是int而声明/推断期望str抛出TypeError。这与普通 Python 中cast完全不做任何运行时检查的行为不同。在编译代码中cast(str, y)实质上等价于if not isinstance(y, str): raise TypeError(...) x y也就是说mypyc 把类型变量检查从构造/转换点推迟到使用点既保证了运行时类型安全又避免了在实例上存储类型标签带来的内存开销。元类只接受行为可静态化的两类大多数元类不受原生类支持因为它们的行为过于动态例如在__new__/__init__中运行时修改类结构。可以使用的元类只有两个abc.ABCMeta—— 抽象基类机制typing.GenericMeta—— 由typing.Generic使用。如果类定义使用了不受支持的元类mypyc 会**放弃将该类编译为原生类**转而把它编译成一个普通的 Python 类。这是 mypyc 的降级策略宁可失去性能优化也不产生语义错误的行为。这一降级机制同样适用于不受支持的类装饰器见下一节它保证了 mypyc 在面对无法静态化的动态特性时是安全失败的。类装饰器白名单之外的都会降级与元类类似大多数类装饰器也不受支持因为它们通常过于动态。可以在原生类上使用的类装饰器只有四个装饰器用途mypy_extensions.trait定义 trait 类型可多继承的特化原生类mypy_extensions.mypyc_attr配置原生类扩展选项见上文继承一节dataclasses.dataclass数据类attr.s(auto_attribsTrue)attrs 库数据类其中数据类dataclass与 attrs 类只有部分原生支持效率不及纯粹的原生类——原因在于它们需要维护__init__、__repr__、__eq__等由元数据动态生成的成员编译器无法像对待手写普通类那样做完全的静态分派。与元类同理如果类定义使用了不在白名单内的类装饰器mypyc 会把该类编译为普通 Python 类。删除属性用__deletable__显式授权默认情况下原生类中定义的属性不能被删除del语句会报错。这是属性表静态化的直接推论编译期生成的访问代码假定属性总是存在。如果确实需要允许删除某些属性可以在类体内通过__deletable__显式声明class Cls: x: int 0 y: int 0 other: int 0 __deletable__ [x, y] # x 和 y 可以被删除 o Cls() del o.x # OK del o.y # OK del o.other # Error__deletable__的使用有严格的形式约束必须在类体内完成初始化其值必须是仅包含字符串字面量的列表或元组表达式且字符串必须指向类中真实存在的属性。以下写法都是非法的a [x, y] class Cls: x: int y: int __deletable__ a # Error: cannot use variable a __deletable__ (a,) # Error: not in a class body第一处错误是因为__deletable__引用了外部变量a——mypyc 要求该列表在编译期即可静态求值不能依赖任何运行时变量第二处错误是因为它位于类体之外mypyc 根本不会把它视为原生类的元数据。实例通常没有__dict__原生类的实例通常不拥有__dict__属性。这与__slots__类的行为一致实例的属性存放在编译器生成的固定槽位中而不是实例字典里。这是原生类属性访问快、内存占用低的根本原因之一也是前文属性集静态固化约束的直接结果——没有__dict__运行时自然无法动态添加属性所以o.extra 3这样的代码才会被拒绝。如果需要动态属性行为原生类不是合适的载体应当把这些类保留在解释执行非编译的模块中。周边语义补充序列化、monkey patching 与受限的 dunder理解原生类还需要掌握几个与之强相关的运行时语义它们记录在 differences_from_python.rst 中直接关系到你能否安全地编译现有代码。pickle / copy 与serializablemypyc 会尽量保证实例通过__init__正确初始化即使对象是通过pickle、copy.copy、copy.deepcopy构造的。如果某个原生类的__init__需要必填参数无法无参调用则该类的实例默认无法被 pickle/copy。可以通过mypyc_attr的serializable标志开启from mypy_extensions import mypyc_attr import pickle mypyc_attr(serializableTrue) class Cls: def __init__(self, n: int) - None: self.n n data pickle.dumps(Cls(5)) obj pickle.loads(data) # OK几个要点所有子类会继承serializable标志设置了allow_interpreted_subclasses的类隐式支持序列化开启序列化可能轻微拖慢属性访问编译代码必须随时准备在属性缺失时抛出AttributeError未设置该标志就 pickle 实例会因__init__缺少参数而抛出TypeError。禁止随意 monkey patching由于 mypyc 的函数与类定义是不可变的无法进行任意 monkey patching——例如在测试中把函数或方法替换为 mock。每个编译模块虽然有一个指向编译函数与类型对象的 Python 命名空间普通的dict可以被修改但编译代码通常不使用这个命名空间所以修改只对非编译代码可见。原生类不支持的 dunder 方法原生类不能使用以下 dunder 方法定义了也不会按预期工作__del____index____getattr__、__getattribute____setattr____delattr__此外原生类不能包含任意的描述符descriptor但property、静态方法staticmethod、类方法classmethod是受支持的。内省与调试限制使用 mypyc 后部分内省手段会失效实例的__annotations__通常不保留编译函数的栈帧无法用inspect检查inspect.ismethod不把编译方法视为方法inspect.signature对编译函数会失败profile、cProfile、trace等剖析/追踪钩子不会触发也无法用pdb在编译函数中设置断点或单步执行。实用的应对方案是在解释模式下完成开发与调试仅在发布阶段编译。推荐实践如何与原生类共处结合文档目录中的 compilation_units.rst 与 getting_started.rst在实践中遵循以下原则可以最大化原生类收益整程序单编译单元一次mypyc调用编译的所有模块构成一个编译单元单元内引用使用早绑定跨编译单元的回退到晚绑定命名空间字典查找 装箱调用性能显著下降。因此尽量把整个程序作为一个编译单元编译减少单元间交互。解释模式开发、编译模式发布开发期用解释模式获得快速编辑-运行循环并用 mypy 全面类型检查实现完成后编译并重跑测试发布编译版本可同时为不支持 mypyc 的平台保留解释版回退。给类变量显式注解ClassVar与Final不只是文档它们决定了编译器把属性放进哪张表、以及能否做常量替换。把动态需求挡在编译边界外需要动态属性、任意装饰器、monkey patching 的代码放在非编译模块性能关键的纯静态类放进编译模块。留意序列化与内省限制需要 pickle 的类显式加mypyc_attr(serializableTrue)需要被外部框架内省的类保持解释执行。总结原生类是 mypyc 性能模型的核心支柱通过把类编译为 C 扩展类、固定属性表、静态化方法分派vtable与早绑定换取了接近内置类型的访问速度代价是丢失普通 Python 类的动态能力。其语义约束可以概括为一句话凡是编译器无法在编译期确定的事情要么被禁止动态属性、任意装饰器、monkey patching要么被降级回退为普通 Python 类要么被推迟到运行边界类型变量检查、序列化。透彻理解 native_classes.rst 中这九组规则你就能准确判断现有 Python 代码中哪些类可以直接受益于 mypyc哪些需要小幅改造从而在保持 Python 开发体验的前提下获得 C 扩展级别的执行效率。进一步阅读本文所依据的完整文档体系位于 mypyc/doc 目录其中 differences_from_python.rst 覆盖运行时类型检查、原语类型与早绑定的全部细节compilation_units.rst 讲解编译单元边界getting_started.rst 提供从安装到发布的完整工作流。【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表