拓冰建站拓冰建站
首页 / 资讯中心 / 正文

mypyc 原生类(Native Classes)完全指南:编译为 C 扩展类的行为差异与限制

mypyc 原生类Native Classes完全指南编译为 C 扩展类的行为差异与限制【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy本文基于 mypyc 官方文档 native_classes.rst 与仓库源码系统讲解 mypyc 编译模块中默认类形态——原生类native class的核心行为不可变命名空间、受限的继承与元类、类变量与泛型语义、mypyc_attr配置、__deletable__属性删除、以及acyclic无环优化等。读完本文你将能判断一个类能否被编译为原生类、如何显式声明或回避原生类限制并针对热路径类正确开启性能优化。什么是原生类在 mypyc 编译的模块中类默认会被编译为原生类即native classes或称为 extension classes。原生类会被直接编译成 C 扩展类CPython C extension class其对象布局、属性访问、方法调用都不再经过普通 Python 类对象机制因而行为与普通 Python 类存在显著差异。从源码看类在中间表示层由 mypyc/ir/class_ir.py 中的ClassIR描述其中is_ext_class True表示编译为原生扩展类# mypyc/ir/class_ir.py class ClassIR: def __init__(self, name, module_name, is_traitFalse, is_generatedFalse, is_abstractFalse, is_ext_classTrue, ...): ...而在 IR 构建阶段mypyc/irbuild/classdef.pyExtClassBuilder扩展类构造器与NonExtClassBuilder非扩展类构造器分别对应原生类与非原生类的代码生成路径if ir.is_ext_class: cls_builder ExtClassBuilder(builder, cdef) # 原生类 else: cls_builder NonExtClassBuilder(builder, cdef) # 非原生类在诸多行为上原生类更接近int、str、list这类内建类型而非普通用户自定义 Python 类。下文逐一展开这些差异。不可变的类命名空间原生类的类型对象命名空间type object namespace基本不可变——你无法在类定义之外替换已有方法也无法给类动态添加新方法。可以修改的只有“类变量”class variable本身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不能动态添加新方法这与使用__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 # OKz 在方法中已声明可赋值 o.extra 3 # Error: no attribute extra也就是说原生类实例的属性集合在“类定义方法体中的赋值”中被静态固定下来。相应的原生类实例通常没有__dict__属性见文档末尾“Other properties”一节这也是其内存更紧凑、访问更快的原因之一。继承规则仅支持单继承原生类之间只支持单继承唯一例外是 trait 类型见 mypyc 文档 mypyc/doc/native_classes.rst 中引用的 trait 类型说明。编译器会在 IR 构建阶段对多继承直接报错见 mypyc/irbuild/classdef.pyif any(ir.base_mro[i].base ! ir.base_mro[i 1] for i in range(len(ir.base_mro) - 1)): builder.error(Multiple inheritance is not supported (except for traits), cdef.line)大多数非原生扩展类non-native extension class不能作为原生类的基类但普通 Python 类可以作为基类前提是它们不使用不受支持的元类见下文“元类”一节。可作为基类的内建扩展类白名单以下非原生扩展类可以充当原生类的基类objectdict以及dict[k, v]BaseExceptionExceptionValueErrorIndexErrorLookupErrorUserWarning在ClassIR中这一支持通过builtin_base字段记录mypyc/ir/class_ir.pyIf this a subclass of some built-in python class, the name of the object for that class. We currently only support this in a few ad-hoc cases.——即对内建基类的支持是特例化的仅限上述列出的少数类型。解释器子类interpreted subclasses需要显式开启默认情况下非原生类不能继承原生类不能在定义原生类的编译单元之外继承它即跨编译单元继承不被允许。如果确有这种需求可以通过mypyc_attr(allow_interpreted_subclassesTrue)显式放开from mypy_extensions import mypyc_attr mypyc_attr(allow_interpreted_subclassesTrue) class Cls: ...允许解释器子类对原生类自身实例的性能影响很小但访问非原生子类或定义在其它编译单元中的子类的方法与属性会更慢因为此时必须回退到普通 Python 属性访问机制。对应的实现事实ClassIR.allow_interpreted_subclasses默认是Falsemypyc/ir/class_ir.py且编译器要求 MRO 链上的所有基类都允许解释器子类否则报错mypyc/irbuild/classdef.pyif ir.allow_interpreted_subclasses: for parent in ir.mro: if not parent.allow_interpreted_subclasses: builder.error( Base class {} does not allow interpreted subclasses.format(parent.fullname), ...)使用mypyc_attr需要安装mypy-extensions包pip install --upgrade mypy-extensions被 mypyc 特殊识别的基类mypyc 还能识别以下基类并理解它们对子类包括原生类行为的改变typing.NamedTupletyping.Generictyping.Protocolenum.Enum这些基类会改变类的运行时形态例如enum.Enum子类会被降级为非扩展类路径见下文。类变量Class Variables原生类中的类变量必须显式声明写法为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!禁止通过实例给类变量赋值提示如果类变量的值是常量可以改用typing.Final或typing.Final[type]声明。在编译器实现上类体内的ClassVar引用会通过builder.class_body_classvars这一字典从“正在构建的类”中解析而不是回退到模块全局变量mypyc/irbuild/classdef.py而__slots__、__deletable__这类特殊属性则被单独处理不当作普通类变量mypyc/irbuild/prepare.py。泛型原生类原生类可以是泛型的。类型变量在运行时会被擦除erased实例不会保存类型变量的具体值。因此编译后的代码无法在运行时检查类型变量的取值——这类检查被推迟到“读取一个类型为类型变量的值”时才进行。示例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类型变量值不做检查 y.item # Runtime error: item is int, but str expected也就是说cast到错误的泛型类型不会在类型转换处报错而是在访问y.item期望str却拿到int时抛出运行时错误。这与 Python 静态类型系统“泛型仅存在于类型层面”的理念一致但在原生类中由于类型变量被完全擦除这一特性被贯彻得更加彻底。元类Metaclasses绝大多数元类不被原生类支持因为其行为过于动态。目前仅支持以下两个元类abc.ABCMetatyping.GenericMeta由typing.Generic使用注意如果某个类定义使用了不受支持的元类mypyc 会把它编译成普通 Python 类非原生类而不是报编译错误。这一点正是“隐式非原生类”的来源之一详见下文。类装饰器Class Decorators与元类类似绝大多数类装饰器也不被原生类支持因为它们通常过于动态。可用的类装饰器有mypy_extensions.trait用于定义 trait 类型mypy_extensions.mypyc_attr见上文“继承规则”dataclasses.dataclassattr.s(auto_attribsTrue)其中dataclasses与attrs类只获得部分原生支持效率不如纯粹的原生类。源码中的实现事实是在 mypyc/irbuild/classdef.py 中mypyc 会按装饰器类型在DataClassBuilder对应dataclasses与attr-auto与AttrsClassBuilder对应attr之间选择构造器且ClassIR.is_augmented注释明确写着An augmented class has additional methods separate from what mypyc generates. Right now the only one is dataclasses.注意如果类定义使用了不受支持的类装饰器mypyc 同样会把它编译成普通 Python 类非原生类。定义非原生类Non-Native Classes显式声明mypyc_attr(native_classFalse)你可以用mypyc_attr(native_classFalse)显式地把某个类定义为普通 Python 类非原生类from mypy_extensions import mypyc_attr mypyc_attr(native_classFalse) class NonNative: def __init__(self) - None: self.attr 1 setattr(NonNative, extra, 1) # Ok这个装饰器只对用 mypyc 编译的类生效。非原生类的效率显著低于原生类但在需要绕过原生类限制时往往是必要的非原生类可以使用任意的元类与类装饰器也支持灵活的多重继承。需要注意的是即便是非原生类mypyc 仍会在编译期对“给未在类体中定义的方法或属性赋值”报错因为这些本来就是 mypy 层面的静态类型错误o NonNative() o.extra x # Static type error: extra not defined但这些操作在运行时依然可行包括在未用 mypyc 编译的模块中也是如此。此外你仍可使用setattr/getattr做任意属性的动态访问Any类型的表达式也不做静态类型检查因而可以访问任意属性a: Any o a.extra x # Ok setattr(o, extra, y) # Also ok隐式非原生类如果被编译的类使用了不受支持的元类或类装饰器它就会隐式地成为非原生类见上文两处“注意”。此时你也可以再用mypyc_attr(native_classFalse)显式标注把意图写清楚。源码中ClassIR的is_ext_class默认是True即默认原生而 mypyc/irbuild/util.py 中的get_mypyc_attrs逻辑会解析类上的mypyc_attr参数集合合法键包括native_class、allow_interpreted_subclasses、serializable、free_list_len、acyclic等# mypyc/irbuild/util.py mypyc_attr_args { native_class, allow_interpreted_subclasses, serializable, free_list_len, acyclic }其中native_classFalse会直接决定该类走非扩展类路径# mypyc/irbuild/util.pyget_mypyc_attrs 内 # Classes with native_classFalse are explicitly marked as non extension. if explicit_native_class is False: ...显式原生类mypyc_attr(native_classTrue)反过来你可以用mypyc_attr(native_classTrue)显式声明某个类必须是原生类。此时如果 mypyc 无法把它编译为原生类就会产生编译期错误而不是静默降级为普通 Python 类# mypyc/irbuild/util.py # Classes with native_classTrue should be extension classes, but they might # not be... if explicit_native_class is True and not implicit_extension_class: # error: fClass is marked as native_classTrue but it cant be a native class. {reason}用这个选项可以避免“不小心定义出隐式非原生类”而性能受损的情况——任何导致降级的原因不受支持的元类、装饰器、继承等都会在编译时被立即暴露。删除属性__deletable__默认情况下原生类中定义的属性不能被删除del o.attr会报错。若想允许删除特定属性可在类体中用__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这些约束在编译器中得到了严格校验。_check_deletable_declarationsmypyc/irbuild/prepare.py会在 prepare 阶段尽早校验使非法程序在构建任何 IR 之前就退出# mypyc/irbuild/prepare.py def _check_deletable_declarations(path, cdef, ir, errors): Validate that attributes listed in __deletable__ refer to definable attributes on the class. Runs in the prepare phase so we exit early on invalid programs before any IR is built. for attr in ir.deletable: if attr not in ir.attributes: if not ir.has_attr(attr): errors.error(fAttribute {attr} not defined, path, line) ... errors.error(fCannot make property {attr} deletable, path, line)对应的测试数据mypyc/test-data/irbuild-classes.test覆盖了各种非法用法例如__deletable__ x # E: __deletable__ must be initialized with a list or tuple expression __deletable__ [1] # E: Invalid __deletable__ item; string literal expected __deletable__ a # E: __deletable__ must be initialized with a list or tuple expression __deletable__ [x] # E: Attribute x not defined in Deriv (defined in Base) __deletable__ [prop] # E: Cannot make property prop deletable从中还可以看到两条额外规则__deletable__中列出的属性必须定义在本类或基类中不能指向未定义属性且属性property不可被设为可删除。另外提示信息Using __deletable__ [attr] in the class body enables del obj.attr说明该用法是编译期静态验证的对应 mypyc/irbuild/statement.py 中对del语句的检查。无环类mypyc_attr(acyclicTrue)默认情况下原生类会参与 CPython 的循环垃圾回收cyclic GC这给对象分配与回收带来一定开销。如果你确定某类实例永远不会成为引用环的一部分可以用mypyc_attr(acyclicTrue)退出循环 GCfrom mypy_extensions import mypyc_attr mypyc_attr(acyclicTrue) class Leaf: def __init__(self, x: int, name: str) - None: self.x x self.name name这样做可以提升性能尤其是对频繁分配与释放的类同时无环实例占用更少内存因为 CPython 无需为它们附加 GC 头GC header。实现事实ClassIR.is_acyclic默认是False并通过序列化字段保存mypyc/ir/class_ir.py 中is_acyclic: self.is_acyclic与ir.is_acyclic data.get(is_acyclic, False)表明该属性会随类 IR 一起持久化。需要特别注意两条边界无环属性不会被继承。每个子类都必须显式使用mypyc_attr(acyclicTrue)才能同样退出循环 GC。误用会导致内存泄漏。警告如果无环类的实例真的参与了引用环这些环将永远不会被回收从而造成内存泄漏。请只对“实例不会引用那些直接或间接又指回本实例的对象”的类使用该选项。其它行为差异原生类实例通常没有__dict__属性。这意味着原生类实例更紧凑、属性访问更快但也意味着不能动态添加任意属性——这是原生类与普通 Python 类最直观的运行时差异之一。总结如何选择类形态把上面的规则归纳成一张决策清单场景建议默认情况无特殊需求直接定义类mypyc 自动编译为原生类需要mypyc_attr系列配置先pip install --upgrade mypy-extensions需要允许解释器子类/跨编译单元继承mypyc_attr(allow_interpreted_subclassesTrue)需要使用任意元类、类装饰器或多重继承mypyc_attr(native_classFalse)显式声明非原生类需要确保类一定是原生类、不允许静默降级mypyc_attr(native_classTrue)无法编译时直接报错热路径上频繁分配/回收且不会成环的类mypyc_attr(acyclicTrue)退出循环 GC需要允许del o.attr的属性在类体中声明__deletable__ [attr, ...]字符串字面量列表/元组原生类是 mypyc 性能收益的核心载体方法调用、属性访问与实例布局都直接映射为 C 级实现代价是类命名空间不可变、继承受限、元类与装饰器受限、无__dict__、属性默认不可删除。理解并善用mypyc_attr与__deletable__这两套显式机制你就能在享受 C 扩展类性能的同时精准掌控类的行为边界。进一步深入可阅读同一目录下的 differences_from_python.rst与 Python 语义的更多差异与 native_operations.rst原生类上的操作实现以及源码 mypyc/ir/class_ir.py 与 mypyc/irbuild/classdef.py 中的对应实现。【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门