如何在Python 3.8-3.14中基于类型注解生成__slots__
问题描述
我想创建基于__slots__的类,不需要重复已经在类型注解里列过的属性名。
在Python 3.14之前,我可以这么实现:
class C: foo: int bar: int __slots__ = (*__annotations__,) not_a_slot: ClassVar[str] = 'some class variable'
但Python 3.14改用了延迟求值注解,这意味着赋值__slots__的时候,__annotations__字典还不存在。另外,Python官方最佳实践也建议3.10之后不要直接用__annotations__。
@dataclass(slots=True)适用于Python 3.10及以上版本,但有很多副作用。可惜我的代码要适配大量遗留系统,必须同时兼容Python 3.8和3.14。
有没有兼容3.8且面向未来的方法,能基于部分注解属性生成__slots__,还不用重复属性名?
我自己找到的唯一办法是写类装饰器生成带__slots__的类,代码如下:
from typing import ClassVar, get_origin try: from inspect import get_annotations except ImportError: def get_annotations(x): return x.__annotations__ def annotations_to_slots(klass): class Out(klass): __slots__ = tuple( name for name, declared_type in get_annotations(klass).items() if get_origin(declared_type) is not ClassVar ) Out.__name__ = klass.__name__ try: Out.__doc__ = klass.__doc__ except AttributeError: pass return Out @annotations_to_slots class C: __slots__ = () # will be filled in by @annotations_to_slots foo: int bar: int not_a_slot: ClassVar[str] = 'not a slot'
这个方法虽然能用,但有几个弊端:__slots__最终在自动生成的类上,而不是定义字段的类;定义类必须写__slots__ = (),就算加了注释还是容易误导;MRO里多了一个类,会让继承关系变模糊。
有没有更优的实现方案?
更优解决方案:使用元类
相比装饰器,元类可以直接在类定义阶段修改原类,避免生成额外的子类,解决你提到的所有弊端。以下是兼容Python 3.8到3.14的实现:
from typing import ClassVar, get_origin import sys try: from inspect import get_annotations except ImportError: def get_annotations(cls): return cls.__annotations__ if hasattr(cls, '__annotations__') else {} class SlotsFromAnnotationsMeta(type): def __new__(cls, name, bases, namespace): # 先创建类的基础实例,确保注解被处理(兼容3.14延迟求值) new_cls = super().__new__(cls, name, bases, namespace) # 获取所有注解,过滤掉ClassVar类型的属性 annotations = get_annotations(new_cls) slots = tuple( attr_name for attr_name, attr_type in annotations.items() if get_origin(attr_type) is not ClassVar ) # 直接给原类设置__slots__,如果已有__slots__则合并(可选) if hasattr(new_cls, '__slots__') and new_cls.__slots__: new_cls.__slots__ = tuple(set(new_cls.__slots__) | set(slots)) else: new_cls.__slots__ = slots return new_cls # 使用示例 class C(metaclass=SlotsFromAnnotationsMeta): foo: int bar: int not_a_slot: ClassVar[str] = 'not a slot' # 测试验证 c = C() c.foo = 1 c.bar = 2 # 下面这行会报错,符合预期,因为not_a_slot是ClassVar不是实例属性 # c.not_a_slot = 'test'
方案优势
- 直接修改原类,
__slots__定义在你编写的类本身,不会生成额外子类,MRO清晰 - 不需要在类里写
__slots__ = (),完全由元类自动生成 - 兼容Python 3.8到3.14:
- 3.8及更早版本用
__annotations__降级处理 - 3.10+使用标准库的
inspect.get_annotations处理延迟求值注解
- 3.8及更早版本用
- 支持合并已有的
__slots__(如果需要手动添加非注解属性的slot)
额外优化:支持排除指定属性
如果需要排除某些注解属性(比如除了ClassVar之外的其他不需要作为slot的属性),可以给类加一个类属性__exclude_from_slots__来指定,元类里读取该属性过滤:
class SlotsFromAnnotationsMeta(type): def __new__(cls, name, bases, namespace): new_cls = super().__new__(cls, name, bases, namespace) annotations = get_annotations(new_cls) exclude = getattr(new_cls, '__exclude_from_slots__', set()) slots = tuple( attr_name for attr_name, attr_type in annotations.items() if get_origin(attr_type) is not ClassVar and attr_name not in exclude ) # 合并已有slots逻辑不变 if hasattr(new_cls, '__slots__') and new_cls.__slots__: new_cls.__slots__ = tuple(set(new_cls.__slots__) | set(slots)) else: new_cls.__slots__ = slots # 移除临时的exclude属性(可选) if '__exclude_from_slots__' in namespace: delattr(new_cls, '__exclude_from_slots__') return new_cls # 使用示例:排除baz属性 class D(metaclass=SlotsFromAnnotationsMeta): foo: int bar: int baz: str __exclude_from_slots__ = {'baz'} not_a_slot: ClassVar[str] = 'class var'
内容的提问来源于stack exchange,提问作者Dan
相关产品推荐
相关产品推荐

