目标PickAndPlaceTask 能表达"红色方块→左侧箱、最多重试 2 次",且类型安全、不可变。

学习dataclass装饰器(默认值/frozen/field)

2.该模块提供装饰功能和自动功能 为用户自定义类添加生成的特殊方法,如和。最初描述了它 在PEP 557中。

这些生成方法中要使用的成员变量定义了 使用PEP 526类型的注释。例如,这段代码:

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

1.@dataclass装饰器自动生成__init__构造函数 下方手写的__init__,等价于装饰器自动帮我们生成的代码,不需要手动写在类里面

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand

2.字段语法:

2.1 name : str 、unit_price : float :没有默认值,实例化必须传入

2.2 quantity_on_hand : int = 0 :设置默认值,不传参自动等于0

3 实例方法 total_cost()

计算库存总价值:单价 × 在库数量。

本地测试:

模块内容

@数据类。dataclass*, init=True, repr=True, eq=True, order=False, unsafe_hash=假冻结=假match_args=真kw_only=假slots=假weakref_slot=False)

该函数是一个装饰器,用于向类添加生成的特殊方法,如下所述。

装饰师检查了该类别以寻找S。A 被定义为具有类型注释的类变量。有两个 下文描述的例外,中没有任何内容检查变量注释中指定的类型。@dataclassfieldfield@dataclass

所有生成方法中场的顺序为 它们在类定义中的出现顺序。

装饰师会添加各种“dunder”方法。 以下将介绍的类别。如果已经有添加的某些方法 存在于该类中,行为依赖于参数,如文档所述 如下。装饰器返回与其被调用的同一类别;没有新消息 类别被创建。@dataclass

如果仅作为无参数的简单装饰器使用, 它表现得好像这里有记录的默认值 签名。也就是说,这三种用法是 等价:@dataclass@dataclass

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False,
           match_args=True, kw_only=False, slots=False, weakref_slot=False)
class C:
    ...

参数如下:@dataclass

1. init: bool = True

  • True:自动生成 __init__() 构造方法,把类字段作为入参初始化实例。
  • 如果手写了 __init__(),本参数直接被忽略,不会自动生成构造函数。

2. repr: bool = True

  • True:自动生成 __repr__(),输出格式:类名(字段1=值1, 字段2=值2,...),按代码定义字段顺序输出。
  • 标记 repr=False 的字段不会出现在打印字符串中。
  • 手写 __repr__() 则该参数失效。

示例输出:

InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)

3. eq: bool = True

  • True:自动生成 __eq__(),用于 == 对象相等比较。
  • Python3.13 行为:逐个字段分别比较 self.a == other.a and self.b == other.b
  • 3.12 及更早版本:把所有字段打包成元组再比较 (self.a,self.b) == (other.a, other.b)

⚠️ 3.13 变更风险:遇到 float("nan") 这类自身不等于自身的值,比较结果会和旧版本不一样。

  • 只能对比同类型实例;手写__eq__()则参数失效。

4. order: bool = False

  • True:自动生成全套比较魔术方法:__lt__ / __le__ / __gt__ / __ge__,支持 < <= > >=
  • 比较逻辑:把实例字段打包元组,按元组规则对比;只能同类型对象对比。
  • 约束:order=True 时,如果eq=False,会强制把eq提升为True
  • 如果手动写了任意一个大小比较魔术方法,直接抛异常,不允许混用。

5. unsafe_hash: bool = False

控制是否自动生成 __hash__(),用于字典 key、set 集合。

对象可哈希要求实例逻辑不可变;可变对象不应该有 hash。

dataclass 自动生成 hash 的默认规则
  1. eq=True && frozen=True:自动生成__hash__
  2. eq=True && frozen=False:设置 __hash__ = None,对象不可哈希(可变对象)
  3. eq=False:不干预__hash__,继承父类 hash(默认是对象 id 哈希)
  • unsafe_hash=True强制生成__hash__,哪怕对象是可变的。属于高级危险用法:对象字段修改后 hash 值会变,放到 dict/set 会彻底错乱。
  • 如果你自己手写了__hash__,不能同时开unsafe_hash=True,会报错。

6. frozen: bool = False

  • frozen=True:冻结实例,实例创建完成后禁止修改实例字段,赋值直接抛异常,模拟只读对象。
  • 如果自己手写__setattr__ / __delattr__,再设置frozen=True会抛出异常。

开启frozen=True + eq=True → dataclass 会自动生成__hash__,实例可以放进 set、作为字典 key。


7. match_args: bool = True(Python3.10 新增)

自动生成 __match_args__ 元组,用于模式匹配 match‑case 语法

  • 保存__init__中非关键字 - only 的形参名。
  • match_args=False,或者手写__match_args__,不会自动生成。

8. kw_only: bool = False(3.10 新增)

  • kw_only=True:所有字段变成仅关键字参数。实例化的时候,必须用关键字传参,不能按位置传参。

python

运行

@dataclass(kw_only=True)
class A:
    x:int
# 合法:A(x=10)
# 非法:A(10)

kw_only 字段不会加入__match_args__

关键记忆总结表

表格

参数默认值核心作用
initTrue自动生成构造__init__
reprTrue自动生成打印__repr__
eqTrue自动生成相等判断__eq__
orderFalse生成全套大小比较运算符
frozenFalse冻结对象,禁止修改属性
unsafe_hashFalse强制生成 hash,可变对象慎用
match_argsTrue支持 match‑case 模式匹配
kw_onlyFalse全部字段强制关键字传参

fields 可以选择性地指定默认值,使用normal Python语法:

@dataclass
class C :
  a : int
  b : int = 0

在本例中,和都将包含在新增的方法中,该方法定义为:a b

def __init__(self, a: int, b: int = 0):

如果字段没有默认值,则 将被提升 跟随一个带有默认值的字段。无论如何,这都成立 发生在单个类中,或因类继承而发生。

数据类field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None

对于常见且简单的使用场景,其他功能都没有 必须。然而,存在一些数据类特征 需要额外的各领域信息。以满足这一需求 补充信息,你可以替换默认字段值 调用提供的函数。例如:field()

@dataclass
class C:
    mylist: list[int] = field(default_factory=list)

c = C()
c.mylist += [1, 2, 3]

如上所示,该值是一个哨兵对象,用于 检测用户是否提供某些参数。这个哨兵是 使用是因为对于某些参数,且 是有效值。 一个明确的含义。没有代码应该直接使用该值。NoneMISSING

参数如下:field()

default

作用:给字段写死一个固定默认值

  • 实例创建的时候,不填这个参数,就自动用这个值。
  • ⚠️不要给列表、字典这种可变东西用这个!所有对象会共用同一个列表,改一个全部都变。
age: int = field(default=18)

不传 age 就自动是 18。

default_factory

作用:动态生成默认值,专门对付列表、字典这类可变数据

  • 必须传一个不带参数的函数,创建对象的时候,调用这个函数生成默认值。
  • ❗不能和 default 一起写,二选一。
tags: list = field(default_factory=list)

每新建一个对象,就调用一次 list (),给你生成全新空列表,各个实例互不干扰。

init

默认 True

  • init=True:这个字段会出现在构造函数 __init__ 的参数里,创建对象的时候可以传值。
  • init=False:构造函数不让传这个字段,内部自己用,代码里手动赋值(一般在__post_init__里面赋值)。

要不要在创建对象的时候给这个变量传参。

uuid: str = field(init=False)
def __post_init__(self):
    self.uuid = "自动生成一串编号"

repr

默认 True

  • repr=True:打印对象的时候,会显示这个字段的值。
  • repr=False:打印对象的时候隐藏这个字段。适合密码、密钥。

打印这个类实例的时候,要不要把这个值显示出来。

password: str = field(repr=False)

hash

默认 None,可选 True / False / None 对象放到 set、做字典 key 的时候,要计算 hash 值。

  • None:跟着 compare 参数走。参与比较就参与哈希,不参与比较就不参与哈希(绝大多数情况用默认)
  • True:强制这个字段参与哈希计算
  • False比较相等要看这个字段,但是算哈希的时候不带它

 正常不用改。特殊场景:这个字段对比要用到,但是算哈希特别慢,就设置hash=False。 注意:这个参数乱改容易出 bug。

compare

默认 True 控制 ==>< 对比两个对象的时候,要不要拿这个字段做对比。

  • compare=True:对比两个对象是否相等,要看这个字段。
  • compare=False:对比相等直接忽略这个字段。

两个对象用 == 判断相等的时候,要不要比较这个变量。

metadata

默认 None 可以塞一个字典。

dataclass 本身完全不看这个数据,存着给别的第三方库读取。比如校验库、序列化库。自己业务代码想存点附加信息放这里。

username: str = field(metadata={"注释":"登录账号","最大长度":20})

kw_only (python3.10 新增)

默认 False

  • kw_only=True:创建对象的时候,这个参数只能关键字传参,不能位置传参

调用的时候只能写 obj(b=xxx),不能直接 obj(100)

@dataclass
class Demo:
    a:int
    b:int = field(kw_only=True)

Demo(10, b=20) # ✅可以
# Demo(10,20) ❌报错,b只能关键字传入

doc(python3.14 新增)

给字段写注释文档,IDE 可以识别这个字段说明。

name:str = field(doc="用户真实姓名")

class dataclasses.Field

Field 对象

⚠️ 你千万不要自己手动 new Field () 对象! Field 对象是 Python 内部自动创建出来的。通过 dataclasses.fields() 函数才能拿到它。

Field 对象公开属性:

  • name:字段名字
  • type:字段标注的类型
  • defaultdefault_factoryinitreprhashcomparemetadatakw_only 和你调用 field(...) 传入的参数完全一样,存的就是那些值。

还有别的内部属性,但属于私有实现,业务代码不要读取、不要依赖,版本升级可能直接变。

class dataclasses.InitVar[T]

只用于构造函数的伪字段

InitVar[int] 写的变量,不是真正实例属性,叫「伪字段」:

  1. 只会加到 __init____post_init__ 的参数里;
  2. fields() 获取字段列表时不会返回它
  3. 对象实例上不会生成这个成员变量。

👉 使用场景:有些参数只在创建对象的时候用一下,不需要保存到实例身上。

from dataclasses import dataclass, InitVar

@dataclass
class User:
    name: str
    raw_pwd: InitVar[str]  # 仅初始化时传入,不会变成 self.raw_pwd

    def __post_init__(self, raw_pwd):
        # raw_pwd 只在这里能用,实例没有这个属性
        self.pwd_hash = raw_pwd + "_hash"

创建对象要传 raw_pwd,但是 user.raw_pwd 访问会报错。

dataclasses.fields(class_or_instance)

拿到数据类所有真实字段的元组

入参:可以传数据类本身,或者该类的实例对象。 返回:一堆 Field 对象组成的元组。

⚠️ 不会返回伪字段:ClassVarInitVar。 如果传入的不是 dataclass,直接抛 TypeError

python

运行

from dataclasses import fields
fs = fields(User)
for f in fs:
    print(f.name, f.type)

查看这个 dataclass 有哪些真正的字段,做序列化、遍历字段的时候经常用。

dataclasses.asdict(obj, *, dict_factory=dict)

把 dataclass 对象转成字典。

规则:

  1. 递归转换:嵌套 dataclass、list、tuple、dict 都会递归展开;
  2. 其他类型对象,底层用 copy.deepcopy() 拷贝;
  3. 返回 {字段名:值} 的字典。
from dataclasses import dataclass, asdict

@dataclass
class Point:
     x: int
     y: int

@dataclass
class C:
     mylist: list[Point]

p = Point(10, 20)
print(asdict(p))  # {'x': 10, 'y': 20}

c = C([Point(0, 0), Point(10, 4)])
print(asdict(c))
# {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}

默认是深拷贝,性能开销大。 如果想要浅拷贝版本,自己写推导:

{field.name: getattr(obj, field.name) for field in fields(obj)}

如果传进来不是 dataclass 实例,抛出 TypeError

dataclasses.astuple(obj, *, tuple_factory=tuple)

把 dataclass 对象转元组。

规则和 asdict 几乎一致:嵌套 dataclass、列表、元组递归展开;其余对象执行深拷贝。

from dataclasses import astuple
p = Point(10, 20)
print(astuple(p)) # (10, 20)

浅拷贝手写版本:

tuple(getattr(obj, field.name) for field in fields(obj))

不是 dataclass 实例,抛 TypeError

ataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, ...)

动态创建 dataclass,不用写 class xxx: 代码,运行时拼出一个数据类。

参数简要通俗说明:

  1. cls_name:新类的类名字符串
  2. fields:字段列表,每一项可以是三种格式:
    • 'name' 只写名字,类型默认 Any
    • ('name', 类型) 名字 + 类型
    • ('name', 类型, field(...)) 名字 + 类型 + field 配置对象
  3. bases=():父类元组,继承哪些类
  4. namespace=None:类命名空间,放方法、类变量(字典)
  5. 后面一堆开关 init,repr,eq,order,frozen...@dataclass 装饰器参数一模一样。
  6. module:设置类的 __module__ 属性;默认取调用处的模块名。
  7. decorator(Python3.14 新增):用哪个装饰器来生成 dataclass,默认就是 @dataclass

dataclasses.replace(obj/**changes)

核心作用

拿一个 dataclass 实例,生成一份全新的对象,把你传进去的字段覆盖成新值,原有对象完全不变。

⚠️不是原地修改旧对象,是返回新副本。

官方规则

  1. obj必须是 dataclass 实例 如果传普通类对象,直接抛TypeError

  2. **changes只能传这个数据类存在的字段名 写了不存在的字段名 → TypeError报错。

new_obj = replace(old_obj, name="张三", age=20) # ok
new_obj = replace(old_obj, xxx=999) # xxx不是类字段 → TypeError
  1. 内部会调用类的 __init__ 不是简单复制属性;会走完整初始化流程,__post_init__也会被执行

这点很关键:不是浅拷贝赋值,相当于拿旧实例大部分值 + 你给的新参数,重新__init__造一个新对象。

  1. InitVar 仅初始化变量要注意 如果类里有不带默认值的InitVar[T],调用replace()的时候必须手动传这个参数,不然__init__ / __post_init__拿不到值会报错。

  2. 不能修改 init=False 的字段field(init=False)定义的字段,不能在replace()里赋值。

因为这些字段不在__init__入参列表,传进去直接抛ValueErrorinit=False代表:不允许构造函数设置该字段,replace()底层调用__init__,自然也不让改。

3.init=False字段在 replace 时不会自动复制旧实例的值

大坑! replace()不会把旧对象身上init=False的字段拷贝到新对象。 新对象的init=False字段会走类本身逻辑,大多情况变成未初始化。

官方建议:尽量少用init=False;如果非要用,不要依赖replace(),自己手写拷贝方法。

  1. 补充:标准库copy.replace()泛型函数也支持 dataclass 对象。

示例:

from dataclasses import dataclass,replace,field,InitVar

@dataclass
class User:
    name: str
    age: int

    #init = False :构造函数不能传入,一般在_post_init_赋值
    secret: str = field(init=False)
    token: InitVar[str]

    def __post_init__(self, token: str):
        self.secret = f'sec_{token}'


print("原对象")
u1 = User('Tom', 18, '123456')
print(u1)

print("-------------------------------------------------------")
print("替换对象")
#替换普通字段,initvar字段必须显示传入
u2 = replace(u1, age=20, token='abcdefg')
print(u2)
print("-------------------------------------------------------")
print("源对象未改变")
print(u1)
print("-------------------------------------------------------")
print("_post_init_重新运行,secret被重新生成")
print(u2.secret)

错误1:# ❌错误:尝试修改 init=False 的secret字段

print("# 错误:尝试修改 init=False 的secret字段")
u3 = replace(u1, secret='abcdefg')
print(u3)

运行结果:

错误2:❌错误:漏掉 InitVar 参数


print("漏掉 InitVar 参数")
u4 = replace(u1, age=20)
print(u4)

运行结果:__init__缺少token,直接抛异常

注意:

  • replace()不改原对象,必须接收返回值;
  • init=False字段不要指望replace帮你复制,会丢失;
  • 使用InitVar不带默认值,每次replace都要再传一遍;

dataclasses.is_dataclass(obj)

官方语义

返回布尔值:

  • True:传入的是dataclass 类本身或者是 dataclass 的实例对象,子类也返回 True;泛型别名不算。
  • False:普通类、普通实例返回 False。

重点坑is_dataclass() 分不清你传的是「类」还是「类的对象」

例如:

from dataclasses import dataclass, is_dataclass

@dataclass
class Demo:
    x:int

print(is_dataclass(Demo))      # True → 传的是类本身
print(is_dataclass(Demo(10)))  # True → 传的是实例对象

验证:

想要只判断是不是实例对象(排除类本身),就加上判断 not isinstance(obj, type)

def is_dataclass_instance(obj):
    # is_dataclass为真,并且obj不是一个类(type),才是实例
    return is_dataclass(obj) and not isinstance(obj, type)

print(is_dataclass_instance(Demo))       # False,这是类
print(is_dataclass_instance(Demo(10)))  # True,这是实例

运行结果:

拆解逻辑

  1. isinstance(obj, type):如果 obj 是类,返回 True;对象实例返回 False。

  • Demo 是类 → isinstance(Demo, type) → True

  • Demo(10) 是实例 → isinstance(Demo(10), type) → False

所以: is_dataclass(obj) and not isinstance(obj, type) 👉 “是 dataclass,并且它不是一个类” → 就一定是 dataclass 的实例。

记忆口诀

  • is_dataclass():类、实例都返回 True,一锅端;

  • 只判断实例:必须叠加 not isinstance(obj, type)

dataclasses.MISSING

官方描述:MISSING 是一个哨兵标记值,用来代表:没有设置 default,也没有设置 default_factory

通俗理解

MISSING 不是你要传给代码的参数,是 dataclasses 内部用的占位标记。

  • 当你写 field(),既没写 default=xxx,也没写 default_factory=...,那么这个字段的 .default 属性就等于 MISSING
  • 它的含义:此字段没有默认值,实例化时必须手动传参,不能省略

注意:不要把 MISSINGNone 搞混。

  • None:是合法默认值,代表 “默认就是空”
  • MISSING:代表 “压根就没提供默认,必须外部传入”
from dataclasses import dataclass, field, fields, MISSING

@dataclass
class Demo:
    a: int                      # 无默认
    b: int = 10                 # default=10
    c: int = field(default_factory=lambda: 20)
    d: int | None = None        # default=None

flds = fields(Demo)
for f in flds:
    print(f.name, "default=", repr(f.default), "is MISSING?", f.default is MISSING)

  • a:没有默认 → f.default is MISSING → 创建对象必须传 a
  • b:有字面默认值,default=10
  • c:用了 default_factory,此时 f.default 依然是 MISSING,真正的生成逻辑交给工厂函数。

关键点:只要使用 default_factoryfield.default 永远等于 MISSING

什么时候会见到 MISSING

写工具、反射遍历 dataclass 字段时会用到:

for f in fields(obj):
    if f.default is MISSING and f.default_factory is MISSING:
        print(f"字段 {f.name} 没有任何默认值,__init__必须传入")

三个状态区分

  1. default = MISSINGdefault_factory = MISSING无默认,必须传参
  2. default = 某个值:使用普通默认值
  3. default_factory = 可调用对象:运行时调用工厂生成默认值

dataclasses.KW_ONLY

作用:伪字段标记,放在类的注解里,KW_ONLY 后面定义的所有字段,在__init__实例化时只能用关键字传参,不能用位置传参

  • _: KW_ONLY 这一行本身是伪字段:不会生成真正的类成员,名字随便写,惯例用下划线_
  • 出现在 KW_ONLY 之后的所有字段:强制关键字参数;
  • 一个 dataclass 里面只能出现一次 KW_ONLY,写多个直接报错。

示例拆解:

from dataclasses import dataclass, KW_ONLY

@dataclass
class Point:
    x: float
    _: KW_ONLY   # ✨分界线:后面全部强制关键字传参
    y: float
    z: float

p = Point(0, y=1.5, z=2.0)   # 正确:x位置传;y,z必须关键字
# p = Point(0, 1.5, 2.0)    # 报错!y、z不允许位置传参

输出结果:

  • x 在 KW_ONLY 前面:既可以位置传,也可以关键字传

  • yz 在 KW_ONLY 后面:只能关键字传参

伪字段本身完全被忽略:不会出现在fields()、不会生成实例属性,只做分割标记。

exception dataclasses.FrozenInstanceError

当 dataclass 设置 frozen=True,这个类的实例变成冻结对象,不可修改

试图 obj.a = xxx 赋值 或者 del obj.a 删除属性时,抛出该异常。

继承自 AttributeError

示例:

from dataclasses import dataclass

@dataclass(frozen=True)
class User:
    name: str
    age: int

u = User("zhangsan", 20)
# u.age = 99   # 抛出 FrozenInstanceError,不能修改
# del u.name   # 同样抛出 FrozenInstanceError

小知识点frozen=True 对象虽然不能直接赋值,但可以用 replace() 返回新对象实现 “修改”。

from dataclasses import replace
u2 = replace(u, age=99) # ✅生成新实例,原对象不变

frozen 注意点

  1. frozen=True 只是拦截__setattr__/__delattr__
  2. 如果字段是可变对象(list/dict),对象内部元素依然可以改
    @dataclass(frozen=True)
    class Demo:
        arr: list
    
    d = Demo([1,2])
    d.arr.append(3) # ✅不会报错!只是不能替换arr本身,列表内部可变

    输出结果:

快速速记

  1. KW_ONLY(3.10+)
    • 伪分割标记,后面字段强制关键字传参;
    • 一个类最多写 1 次;
    • 惯例命名 _: KW_ONLY,本身不产生字段。
  2. FrozenInstanceError
    • frozen=True 的实例被写 / 删除属性时抛出;
    • 想 “改值” 用replace()生成新对象;
    • 冻结挡不住可变容器内部修改。

dataclasses 完整 API 精简速查表

装饰器

表格

API作用
@dataclass()

自动生成 __init__/__repr__/__eq__/__hash__ 等魔术方法常用参数:

frozen=True:实例冻结,禁止赋值

kw_only=True全部字段强制关键字传参

repr=False:不生成__repr__

eq=False:不生成__eq__

order=True:生成比较运算符

field () 字段配置

from dataclasses import field
field(
    default=...,        # 字段普通默认值;和default_factory互斥
    default_factory=...,# 零参数可调用对象,运行时生成可变默认(list/dict)
    init=True,          # 是否加入 __init__ 参数;init=False 不能用replace修改
    repr=True,          # 是否放进repr输出
    hash=None,          # 是否参与hash计算;None跟随compare
    compare=True,       # 是否参与 == > < 比较
    metadata={},        # 附加自定义元数据字典,不参与运行逻辑
    kw_only=False       # 该字段单独强制关键字传参(3.10+)
)

运行时工具函数

函数功能关键点
fields(obj)返回字段 Field 对象元组支持类 / 实例;普通对象抛异常
is_dataclass(obj)是否 dataclass(类或者实例都返回 True)不能区分类 / 实例;判断实例:is_dataclass(obj) and not isinstance(obj,type)
replace(obj,**changes)基于旧实例生成全新 dataclass 对象底层调用__init__+__post_init__❌不能修改init=False字段;不会复制 init=False 旧值❌InitVar无默认必须手动传入
asdict(obj)dataclass 实例转为字典递归转换嵌套 dataclass
astuple(obj)dataclass 实例转为元组递归转换嵌套 dataclass
make_dataclass()动态创建 dataclass 类运行时动态造类,极少使用

特殊类型 / 哨兵常量

名称说明
InitVar[T]仅初始化伪字段,只传给__init____post_init__不会成为实例属性;replace()必须显式传入无默认 InitVar
KW_ONLY(3.10+)伪字段分割标记;其后所有字段强制关键字传参惯例写法:_: KW_ONLY;一个类只能出现一次
MISSING内部哨兵;代表没有 default、没有 default_factory⚠️不等于None;业务代码不要拿来赋值

异常

异常触发场景
FrozenInstanceErrorfrozen=True,执行obj.x=val / del obj.x;继承AttributeError

Field 对象属性(fields()返回)

f = fields(obj)[0]
f.name          # 字段名字
f.type          # 字段类型注解
f.default       # 默认值,无默认则=MISSING
f.default_factory
f.init/repr/hash/compare/metadata/kw_only

高频踩坑清单

  1. replace()返回新对象,不会修改原对象init=False字段不会拷贝旧值
  2. frozen=True仅拦截属性赋值,list/dict 内部元素依旧可变
  3. default_factory只能传无参函数,不能传 lambda 带参数
  4. is_dataclass()类、实例均返回 True,判断实例必须加not isinstance(obj,type)
  5. InitVar不会变成实例成员,只在初始化阶段生效
  6. MISSING≠None,MISSING 代表完全没有配置默认
  7. KW_ONLY只是分割标记,本身不生成实例属性

       

enum枚举

枚举:

  • 是一组符号名称(成员),绑定于唯一的值

  • 可以迭代以返回其典范(即非别名)成员 定义顺序

  • 使用调用语法按值返回成员

  • 使用索引语法按名称返回成员

枚举可以通过语法创建,或通过 使用函数调用语法:

from enum import Enum

# class syntax
class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

# functional syntax
Color = Enum('Color', [('RED', 1), ('GREEN', 2), ('BLUE', 3)])

Enum

所有枚举的基类

实例属性(每个枚举成员身上)

1.    .name

枚举成员定义时的名字。

Color.BLUE.name
# 'BLUE'
2.   .value

枚举成员绑定的值。

Color.RED.value
# 1

可以在 __new__() 方法里面修改这个值。

枚举成员的值可以是 int、str 任意类型。如果不在乎具体数值,可以用 auto(),让系统自动帮你生成值。

虽然可以用 dict、list 这类可变、不可哈希对象作为枚举值,但创建枚举的时候性能会急剧变差(平方级耗时),尽量不要用可变对象做枚举值。

3.   _name_: 内部属性,等价于 .name
4.   _value_: 内部属性,等价于.value
5.   _order_(已废弃,仅为向前兼容保留)

Python2 时代产物,Python3 不再需要。 用来强制校验枚举定义顺序和字符串列表是否一致,不一致直接抛 TypeError

示例:

from enum import Enum


class Color(Enum):
    _order_ = 'RED GREEN BLUE'
    RED = 1
    BLUE = 3
    GREEN = 2

报错:定义顺序和_order_写的顺序对不上

6._ignore_

写一个名字列表,列表里的名字不会变成枚举成员,类构建完成后这些名字直接从枚举类删掉。 用于把辅助变量、工具函数排除掉,不让它们变成枚举项。

特殊魔术方法/钩子方法

_dir_(self)

调用 dir(枚举成员实例) 返回的属性列表。 默认包含:namevalue,以及你写在枚举类上的类方法。

示例:你写了 @classmethod today()dir(Weekday.SATURDAY) 就能看到 today

_generate_next_value_(name, start, count, last_values) 【静态方法】

配合 auto() 使用,自定义 auto 自动生成的值。 参数:

  • name:当前正在定义的枚举成员名字
  • start:起始值,默认 1
  • count:当前已经定义了多少个成员(不包含当前这一个)
  • last_values:前面所有已经生成过的值列表

默认行为:

  • 普通 Enum:在上一个最大值基础上 +1
  • Flag 枚举:生成 2 的幂次(1,2,4,8…)

自定义示例:每次返回 3 的 n 次方

from enum import auto, Enum
class PowersOfThree(Enum):
    @staticmethod
    def _generate_next_value_(name, start, count, last_values):
        return 3 ** (count + 1)
    FIRST = auto()
    SECOND = auto()

print(PowersOfThree.SECOND.value) # 9
_init_(self,*args,**kwds)

默认什么都不干

如果枚举赋值写了多个值,会全部传给_init_.

class Weekday(Enum):
    MONDAY = 1, 'Mon'
# 等价调用 __init__(self, 1, "Mon")
_init_subclass_(cls,**kwds)

子类钩子,继承Enum的子类创建时触发,默认不做事情,可以重写做自定义配置。

_missing_(cls,value)

按值查找枚举找不到的时候,会调用这个钩子,用来实现自定义匹配逻辑。

比如忽略大小写、模糊匹配。返回找到的枚举成员;找不到返回None

示例:字符串枚举忽略大小写查找

from enum import auto, StrEnum
class Build(StrEnum):
    DEBUG = auto()
    OPTIMIZED = auto()

    @classmethod
    def _missing_(cls, value):
        value = value.lower()
        for member in cls:
            if member.value == value:
                return member
        return None

print(Build("deBUG")) # <Build.DEBUG: 'debug'>

输出结果:

_new_(cls,*args,**kwds)

默认不存在。如果你重写它,赋值的全部参数都会传入。

注意:自定义__new__不要写super().__new__(),直接调用对应类型的__new__。 比如继承int, Enum,就调用 int.__new__(...)

_repr_(self)

控制repr(枚举实例) 的输出。默认输出<类名.成员名:value>,可以重写。

_str_(self)

控制str(枚举实例)的输出。默认输出类名.成员名,可重写。

_format_(self)

控制f-string/format()的格式化输出,默认调用_str_,可以重写。

_add_alias(name)

给已存在的枚举成员增加别名。同一个枚举值,多个名字。

Color.RED._add_alias_("ERROR")
Color.ERROR   # 等价于 Color.RED

如果这个名字已经被别的成员占用,抛出NameError

_add_value_alias_(value)

给已有的枚举成员增加值别名。用这个新值也能查到该枚举。

Color.RED._add_value_alias_(42)
Color(42)  # 返回 Color.RED
Enum 极简速查小卡片
项目说明
.name枚举成员的名字字符串
.value枚举成员绑定的值
auto()自动生成值,默认从 1 开始自增
_generate_next_value_重写这个静态方法,自定义 auto 生成规则
_ignore_名字列表,列表内的名字不生成枚举成员
_missing_按值查找失败时触发,做模糊 / 兼容查找
__new__控制枚举实例创建,处理传入的赋值参数
__init__枚举成员初始化,多值赋值会拆成多个参数传入
_add_alias_()3.13+,增加名字别名
_add_value_alias_()3.13+,增加值别名

IntEnum

IntEnum 继承 Enum枚举成员同时也是整数,可以直接参与整数运算;运算之后得到普通 int,不再是枚举对象。

from enum import IntEnum

class Number(IntEnum):
    ONE = 1
    TWO = 2
    THREE = 3

关键特性演示:

# 原始枚举成员,保留枚举身份
print(Number.THREE)          # Number.THREE
print(repr(Number.THREE))    # <Number.THREE: 3>

# ✅可以直接做整数加减运算
print(Number.ONE + Number.TWO)   # 3  返回普通int,不再是Number枚举
print(Number.THREE + 5)         # 8

# ✅直接和数字做相等比较
print(Number.THREE == 3)        # True

# ⚠️运算结果丢失枚举类型
res = Number.ONE + Number.TWO
print(type(res))                # <class 'int'>,不是Number

输出结果:

和普通Enum核心区别

from enum import Enum
class NumEnum(Enum):
    ONE =1

print(NumEnum.ONE == 1)   # False 普通Enum不等于数字
print(Number.ONE ==1)     # True  IntEnum等于数字

auto()在IntEnum

使用auto()自动赋值,从1开始自增整数

from enum import IntEnum, auto
class AutoNum(IntEnum):
    A = auto()   # 1
    B = auto()   # 2
    C = auto()   # 3
  • IntEnum.__str__() 改成和int.__str__()行为一致,str(Number.THREE)得到字符串"3"
  • __format__()同样对齐 int,目的:更好替代项目里零散数字常量。
# 3.11+行为
str(Number.THREE)   # '3'  老版本输出 'Number.THREE'
f"{Number.THREE}"   # '3'

StrEnum

StrEnum 是字符串枚举(Python 3.11 + 新增),成员既是枚举实例,同时又是字符串,可以直接参与字符串操作;但字符串运算后的返回值是普通字符串,不再属于枚举对象。

from enum import StrEnum, auto

class Color(StrEnum):
    RED = 'r'
    GREEN = 'g'
    BLUE = 'b'
    UNKNOWN = auto()
运行示例:
print(Color.RED)                # Color.RED
print(repr(Color.RED))          # <Color.RED: 'r'>

# auto():值为**成员名字小写字符串**
print(Color.UNKNOWN)            # <Color.UNKNOWN: 'unknown'>
print(str(Color.UNKNOWN))       # 'unknown'

# 可以直接做字符串运算,运算结果变成普通str,丢失枚举身份
s = Color.RED + "_color"
print(s)                        # r_color
print(type(s))                  # <class 'str'>,不再是Color枚举

输出结果:

auto()行为重点

StrEnum 使用 auto(),自动取值为枚举成员的小写

class Demo(StrEnum):
    HELLO = auto()   # 值 = "hello"
    TEST_DATA = auto() # 值 = "test_data"

设计改动

  • __str____format__ 对齐原生 str 的行为
  • str(Color.RED) 直接拿到字符串'r',目的:用来替换项目里零散硬编码字符串常量。

非常重要的坑

部分标准库代码写的是严格类型判断:type(x) == str,而不是 isinstance(x, str)StrEnum 的成员是str的子类,这种严格相等校验会失败。

解决:手动转字符串
# 遇到type(x) == str校验的地方,强制str()包裹
some_func(str(Color.RED))

和普通Enum对比

from enum import Enum

class ColorNormal(Enum):
    RED = "r"

print(ColorNormal.RED == "r")   # False 普通Enum不等于字符串
print(Color.RED == "r")         # True StrEnum可以直接和字符串相等

适用场景

API 字符串状态、命令字符串、请求类型、协议字符串常量。

class ApiStatus(StrEnum):
    SUCCESS = auto()
    FAIL = auto()
    PENDING = auto()

# 直接可以放到json、header,比较字符串
resp = {"status": ApiStatus.SUCCESS}

auto()

auto() 用来代替手写枚举值,枚举框架会调用 _generate_next_value_() 自动生成对应值,不同枚举类型生成规则不一样

各枚举类型auto()默认规则

1.Enum/IntEnum:上一个值 +1
from enum import Enum, IntEnum, auto
class E(IntEnum):
    A = auto()   # 1
    B = auto()   # 2
    C = 10
    D = auto()   # 11

输出结果:

2. Flag / IntFlag: 取大于当前最大值的 2的幂(位运算)

from enum import IntFlag, auto
class F(IntFlag):
    A = auto() # 1 (0b001)
    B = auto() # 2 (0b010)
    C = auto() # 4 (0b100)

输出结果:

3. StrEnum: 把成员名字转小写字符串

from enum import StrEnum, auto
class S(StrEnum):
    HELLO = auto() # "hello"
    TEST_DATA = auto() # "test_data"

输出结果:

风险:手动写的值和 auto() 混用要小心,auto 会顺着上一个实际值往后算,容易出现意料之外的值。

auto()生效条件(重点)

auto()只有直接出现在赋值顶层才会被解析。

可以正常工作

FIRST = auto()                     # 直接赋值auto()
SECOND = auto(), -2                # 元组,auto在顶层,会被解析

不生效,不会自动生成值

THREE = [auto(), -3]   # 放到列表里面,auto不会被展开,存的是原始auto对象

自定义 auto 的生成逻辑:重写 _generate_next_value_

可以覆写这个类方法,自己控制 auto 产出什么值。

from enum import Enum, auto

class MyEnum(Enum):
    @staticmethod
    def _generate_next_value_(name, start, count, last_values):
        # name:枚举成员名字字符串
        # start:起始值
        # count:已经定义多少个成员
        # last_values:前面所有成员的值列表
        return f"prefix_{name.lower()}"

    AAA = auto()
    BBB = auto()

print(MyEnum.AAA.value) # prefix_aaa
print(MyEnum.BBB.value) # prefix_bbb

输出结果:

成员遍历

1.直接遍历枚举类本身(最常用)

直接for循环枚举类对象,迭代所有枚举成员实例

from enum import IntEnum, StrEnum, auto

class Color(IntEnum):
    RED = 1
    GREEN = 2
    BLUE = 3

# 遍历成员对象
for member in Color:
    print(f"name={member.name}, value={member.value}")

输出结果:

遍历得到的是枚举实例 Color.RED,不是数字 / 字符串。

2.list()转列表

members = list(Color)
print(members)
# [<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]

输出结果:

3_members_属性(字典,包含别名)

Enum.__members__ 返回有序字典:{成员名字符串: 枚举实例}

__members__会把别名也一并返回;普通 for 遍历会跳过别名。

for name, member in Color.__members__.items():
    print(name, member.value)

别名演示:

class Color(IntEnum):
    RED = 1
    R = 1   # 这是RED的别名

print(list(Color))          # 只输出 RED,别名R被跳过
print(list(Color.__members__.items())) # RED、R全部拿到

输出结果:

        

✅普通for 枚举类:只遍历真正成员,过滤别名

__members__:拿到全部,包含别名,适合做反向查找

4.StrEnum遍历示例:

from enum import StrEnum, auto
class Status(StrEnum):
    OK = auto()
    FAIL = auto()

for m in Status:
    print(m.name, m.value, str(m))

5.常用遍历衍生写法

提取全部name

names = [m.name for m in Color]
# ['RED', 'GREEN', 'BLUE']

提取全部value

values = [m.value for m in Color]
# [1,2,3]

name->menber反向查找

member = Color["RED"]   # 通过名字字符串拿成员
print(member.value)

value->member反向查找

member = Color(1)
print(member.name) # RED

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐