• 微信:WANCOME
  • 扫码加微信,提供专业咨询
  • 服务热线
  • 13215191218
    13027920428

  • 微信扫码访问本页
Python 之 dataclasses
Python 之 dataclasses 模块完全指南

Python 之 dataclasses 模块完全指南

dataclasses 是 Python 3.7+ 引入的标准库模块,用于简化数据类的定义。它通过自动生成 __init__、__repr__、__eq__ 等特殊方法,大幅减少编写简单数据类时的样板代码。


一、基本概念

传统方式 vs dataclass

# ❌ 传统方式:样板代码太多
class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age
    
    def __repr__(self):
        return f"Person(name={self.name!r}, age={self.age!r})"
    
    def __eq__(self, other):
        if not isinstance(other, Person):
            return False
        return (self.name, self.age) == (other.name, other.age)

# ✅ dataclass 方式:简洁明了
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int

二、核心装饰器参数

@dataclass 装饰器接受以下参数:

参数 默认值 说明
init True 是否生成 __init__ 方法
repr True 是否生成 __repr__ 方法
eq True 是否生成 __eq__ 方法
order False 是否生成比较方法(<, <=, >, >=)
unsafe_hash False 是否生成 __hash__ 方法
frozen False 是否创建不可变对象

1. order=True:启用排序

@dataclass(order=True)
class Product:
    price: float
    name: str = ""

p1 = Product(100, "A")
p2 = Product(200, "B")
print(p1 < p2)        # True(按 price 比较)
print(p1 <= p2)       # True
print(p1 > p2)        # False

# 自定义排序字段
@dataclass(order=True)
class Student:
    score: int
    name: str
    
    # 可选:指定排序依据
    def __post_init__(self):
        self._sort_key = (self.score, self.name)

2. frozen=True:不可变对象

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)
# p.x = 10  # ❌ FrozenInstanceError: cannot assign to field 'x'

3. unsafe_hash=True:使实例可哈希

@dataclass(unsafe_hash=True)
class HashablePoint:
    x: int
    y: int

point_set = {HashablePoint(1, 2), HashablePoint(3, 4)}  # 可用作字典键

三、字段定义与 field() 函数

基础字段定义

from dataclasses import dataclass, field
from typing import List, Optional

@dataclass
class User:
    # 必填字段
    username: str
    
    # 有默认值的字段(必须放在必填字段之后)
    age: int = 18
    email: str = ""
    
    # 使用 field() 定义字段
    tags: List[str] = field(default_factory=list)  # 避免可变默认值
    metadata: dict = field(default_factory=dict)
    temp_data: Optional[str] = field(default=None, init=False)  # 不参与初始化

field() 函数完整参数

参数 说明
default 字段默认值
default_factory 提供默认值的无参函数(用于可变对象)
init 是否作为 __init__ 参数(默认 True)
repr 是否包含在 __repr__ 中(默认 True)
compare 是否参与比较(默认 True)
hash 是否参与哈希计算(默认 None,使用 compare 值)
metadata 附加信息(不被 dataclass 处理)

高级字段示例

@dataclass
class AdvancedExample:
    # 1. 隐藏敏感信息
    password: str = field(repr=False)  # __repr__ 中不显示
    
    # 2. 不参与比较
    cache: dict = field(default_factory=dict, compare=False)
    
    # 3. 临时计算字段(不参与初始化)
    created_at: float = field(init=False, default_factory=time.time)
    
    # 4. 带元数据的字段
    value: int = field(metadata={"unit": "meters", "min": 0})

四、特殊方法

1. __post_init__:初始化后处理

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # 不在 __init__ 中
    
    def __post_init__(self):
        """在 __init__ 执行后自动调用"""
        if self.width <= 0 or self.height <= 0:
            raise ValueError("Width and height must be positive")
        self.area = self.width * self.height

rect = Rectangle(10, 20)
print(rect.area)  # 200

2. 继承中的 __post_init__

@dataclass
class Base:
    x: int
    
    def __post_init__(self):
        print(f"Base __post_init__: x={self.x}")

@dataclass
class Derived(Base):
    y: int
    
    def __post_init__(self):
        super().__post_init__()  # 必须显式调用父类方法
        print(f"Derived __post_init__: y={self.y}")

d = Derived(10, 20)
# 输出:
# Base __post_init__: x=10
# Derived __post_init__: y=20

五、继承与字段顺序

字段继承规则

@dataclass
class Animal:
    species: str

@dataclass
class Dog(Animal):
    name: str
    age: int = 0

# 字段顺序:父类字段在前,子类字段在后
dog = Dog("犬科", "旺财", 3)
print(dog)  # Dog(species='犬科', name='旺财', age=3)

# 错误的字段顺序(子类默认值前不能有父类无默认值字段)
@dataclass
class Cat(Animal):
    age: int = 0    # ✅ 正确:有默认值字段在最后
    name: str       # ❌ 错误:有默认值后不能跟无默认值字段

六、实用函数

1. asdict() 和 astuple()

from dataclasses import asdict, astuple

@dataclass
class Point:
    x: int
    y: int

p = Point(1, 2)
print(asdict(p))   # {'x': 1, 'y': 2}(递归转换嵌套对象)
print(astuple(p))  # (1, 2)

2. replace():创建修改副本

from dataclasses import replace

p1 = Point(1, 2)
p2 = replace(p1, x=10)  # 创建新对象,不修改原对象
print(p1)  # Point(x=1, y=2)
print(p2)  # Point(x=10, y=2)

# 适用于 frozen=True 的类
@dataclass(frozen=True)
class ImmutablePoint:
    x: int
    y: int

p = ImmutablePoint(1, 2)
p2 = replace(p, y=5)  # ✅ 唯一修改不可变对象的方法

3. fields():获取字段信息

from dataclasses import fields

@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False

for f in fields(Config):
    print(f.name, f.type, f.default)
# host <class 'str'> localhost
# port <class 'int'> 8080
# debug <class 'bool'> False

4. is_dataclass():检查是否为 dataclass

from dataclasses import is_dataclass

print(is_dataclass(Point))     # True(类)
print(is_dataclass(Point(1,2))) # True(实例)
print(is_dataclass(str))       # False

七、常见陷阱与解决

陷阱1:可变默认值

# ❌ 错误:所有实例共享同一个列表
@dataclass
class Wrong:
    items: list = []

w1 = Wrong()
w2 = Wrong()
w1.items.append(1)
print(w2.items)  # [1] ← 意外共享!

# ✅ 正确:使用 default_factory
@dataclass
class Correct:
    items: list = field(default_factory=list)

陷阱2:默认值字段位置

# ❌ 错误:有默认值的字段不能放在无默认值字段之前
@dataclass
class Wrong:
    age: int = 18      # 有默认值
    name: str          # 无默认值(SyntaxError)

# ✅ 正确
@dataclass
class Correct:
    name: str          # 无默认值在前
    age: int = 18      # 有默认值在后

陷阱3:类属性污染

# ❌ 错误:类属性会变成默认值
@dataclass
class Wrong:
    items: list
    items = []  # 这不会产生预期效果

# ✅ 正确:在类级别定义常量
@dataclass
class Correct:
    items: list
    MAX_SIZE: int = 100  # 类常量(不会成为实例字段)

八、实际应用示例

1. 配置管理

@dataclass(frozen=True)
class DatabaseConfig:
    host: str = "localhost"
    port: int = 5432
    username: str
    password: str
    database: str

# 部分配置使用默认值
config = DatabaseConfig(
    username="admin",
    password="secret",
    database="myapp"
)

2. API 响应模型

from typing import List, Optional

@dataclass
class Address:
    city: str
    street: str
    zipcode: str

@dataclass
class User:
    id: int
    name: str
    email: str
    address: Optional[Address] = None
    tags: List[str] = field(default_factory=list)
    
    def __post_init__(self):
        if "@" not in self.email:
            raise ValueError(f"Invalid email: {self.email}")

3. 树节点结构

@dataclass
class Node:
    value: int
    left: 'Node' = None
    right: 'Node' = None
    
    def __post_init__(self):
        if self.value < 0:
            raise ValueError("Value must be non-negative")

# 构建二叉树
root = Node(5, Node(3), Node(7))

4. 状态机状态

from enum import Enum
from typing import Any

class State(Enum):
    PENDING = "pending"
    RUNNING = "running"
    COMPLETED = "completed"
    FAILED = "failed"

@dataclass
class Task:
    name: str
    state: State = State.PENDING
    result: Any = None
    error: Optional[str] = None
    retries: int = 0
    
    def complete(self, result):
        self.state = State.COMPLETED
        self.result = result
    
    def fail(self, error: str):
        self.state = State.FAILED
        self.error = error

九、dataclass vs 其他方案

特性 dataclass 普通类 namedtuple Pydantic
代码量 少 多 极少 少
可变性 可配置 可变 不可变 可配置
类型提示 ✅ 可加 ❌ ✅
运行时验证 ❌ 手动 ❌ ✅
序列化 手动 手动 手动 内置
JSON 支持 需第三方 需第三方 需第三方 内置
性能 好 好 更好 一般
依赖 标准库 无 标准库 第三方

十、性能考虑

# dataclass 访问速度
@dataclass
class DataClassPoint:
    x: int
    y: int

# 普通类访问(类似速度)
class RegularPoint:
    __slots__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

# 使用 __slots__ 节省内存
@dataclass
class SlottedPoint:
    __slots__ = ('x', 'y')
    x: int
    y: int

十一、总结

要点 说明
核心优势 减少样板代码,自动生成特殊方法
适用场景 数据传输对象(DTO)、配置类、值对象
必须注意 可变默认值用 default_factory,字段顺序规则
扩展能力 __post_init__ 实现自定义逻辑
组合使用 可与枚举、类型提示、抽象基类等配合

dataclass 是 Python 中处理数据类的最佳实践,它让代码更简洁、更安全、更易维护。对于大多数存储数据的类,优先考虑使用 dataclass 而不是手写 __init__ 方法。