首页 / 知识库 / 0基础入门-阅读资料 / 0基础-python入门到精通

第 16 章 dataclasses 与现代类定义

16.1 @dataclass 基本用法

dataclass 自动生成 __init____repr____eq__ 等样板代码:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

# 自动生成的等价代码:
# def __init__(self, x: float, y: float):
#     self.x = x
#     self.y = y
# def __repr__(self):
#     return f"Point(x={self.x!r}, y={self.y!r})"
# def __eq__(self, other):
#     return (self.x, self.y) == (other.x, other.y)

p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)

print(p1)          # Point(x=3.0, y=4.0)
print(p1 == p2)    # True

@dataclass 参数

@dataclass(
    init=True,        # 生成 __init__(默认 True)
    repr=True,        # 生成 __repr__(默认 True)
    eq=True,          # 生成 __eq__(默认 True)
    order=False,      # 生成 __lt__, __le__, __gt__, __ge__(默认 False)
    unsafe_hash=False, # 生成 __hash__(默认 False)
    frozen=False,     # 不可变(默认 False)
    match_args=True,  # 生成 __match_args__(3.10+)
    kw_only=False,    # 所有字段仅限关键字(3.10+)
    slots=False,      # 使用 __slots__(3.10+)
)
class Config:
    pass

16.2 field() 与默认工厂

from dataclasses import dataclass, field

@dataclass
class Student:
    name: str
    age: int = 18                            # 简单默认值
    grades: list[float] = field(default_factory=list)  # 可变默认值用工厂
    _id: int = field(default=0, repr=False)  # 不在 repr 中显示
    school: str = field(default="Unknown", compare=False)  # 不参与比较

s1 = Student("Alice", 20, [90.0, 85.0])
s2 = Student("Alice", 20, [90.0, 85.0], school="MIT")
print(s1)          # Student(name='Alice', age=20, grades=[90.0, 85.0])
print(s1 == s2)    # True — school 不参与比较

field() 参数

参数说明默认
default默认值
default_factory默认值工厂函数
repr是否在 repr 中显示True
compare是否参与比较True
hash是否参与 hashNone
init是否包含在 initTrue
kw_only是否为仅限关键字参数False
metadata附加元数据None
from dataclasses import dataclass, field, fields

@dataclass
class Column:
    name: str
    type: str = "VARCHAR"
    max_length: int = field(default=255, metadata={"db": True})
    nullable: bool = field(default=True, metadata={"db": True})

# 读取元数据
for f in fields(Column):
    if f.metadata.get("db"):
        print(f"{f.name}: metadata={f.metadata}")

16.3 __post_init__ 后处理

__init__ 之后自动调用,用于派生属性和验证:

from dataclasses import dataclass, field
import math

@dataclass
class Circle:
    radius: float
    area: float = field(init=False)         # 不在 __init__ 中
    circumference: float = field(init=False)
    
    def __post_init__(self):
        if self.radius < 0:
            raise ValueError("半径不能为负")
        self.area = math.pi * self.radius ** 2
        self.circumference = 2 * math.pi * self.radius

c = Circle(5)
print(c)
# Circle(radius=5, area=78.539..., circumference=31.415...)
# 使用 InitVar 传递仅用于初始化的参数
from dataclasses import dataclass, InitVar

@dataclass
class User:
    name: str
    password: InitVar[str]   # 只传给 __post_init__,不存为属性
    password_hash: str = field(init=False)
    
    def __post_init__(self, password):
        import hashlib
        self.password_hash = hashlib.sha256(password.encode()).hexdigest()

user = User("Alice", "secret123")
print(user)  # User(name='Alice', password_hash='...')
# user.password  # AttributeError — 不存储明文密码

16.4 frozen 不可变数据类

@dataclass(frozen=True)
class FrozenPoint:
    x: float
    y: float

p = FrozenPoint(3, 4)
# p.x = 10  # FrozenError: cannot assign to field 'x'

# frozen 数据类自动可哈希
points = {FrozenPoint(0, 0): "origin", FrozenPoint(1, 1): "diagonal"}
print(FrozenPoint(0, 0) in points)  # True

16.5 继承与字段排序

@dataclass
class Base:
    x: int
    y: int = 0

@dataclass
class Child(Base):
    z: int = 0        # 子类的有默认值的字段必须放在后面
    w: int = 0

c = Child(1, 2, 3, 4)
print(c)  # Child(x=1, y=2, z=3, w=4)

# 注意:父类无默认值 + 子类有默认值的顺序问题
@dataclass
class Parent:
    name: str
    value: int = 0    # 有默认值

# @dataclass
# class BadChild(Parent):
#     required: int   # 无默认值 — TypeError! 无默认值字段不能在有默认值字段之后

# 解决方案:使用 kw_only(Python 3.10+)
@dataclass
class GoodChild(Parent):
    required: int = field(kw_only=True)

c = GoodChild("test", required=42)

16.6 __slots__ 与 dataclass 结合(Python 3.10+)

@dataclass(slots=True)
class SlottedPoint:
    x: float
    y: float

import sys
p = SlottedPoint(3, 4)
print(sys.getsizeof(p))   # 比无 slots 版本小得多

# 不能添加额外属性
# p.z = 5  # AttributeError

16.7 与 NamedTuple 的对比与选择

from typing import NamedTuple
from dataclasses import dataclass

# NamedTuple 方式
class PointNT(NamedTuple):
    x: float
    y: float

# dataclass 方式
@dataclass
class PointDC:
    x: float
    y: float

# frozen dataclass 方式
@dataclass(frozen=True)
class PointFDC:
    x: float
    y: float
特性NamedTupledataclassfrozen dataclass
可变性不可变可变不可变
继承自tupleobjectobject
索引访问p[0]不支持不支持
解包x, y = p不支持不支持
可哈希否(默认)
slots可选可选
默认值支持支持支持
post_init不支持支持支持
方法可以添加可以添加可以添加
内存更小较大较大(用 slots 接近)

选择指南

# 需要元组特性(解包、索引、不可变)→ NamedTuple
point = PointNT(3, 4)
x, y = point      # 解包
distance = (point[0]**2 + point[1]**2) ** 0.5  # 索引

# 需要可变对象或复杂逻辑 → dataclass
@dataclass
class GameEntity:
    x: float = 0
    y: float = 0
    health: int = 100
    
    def move(self, dx, dy):
        self.x += dx
        self.y += dy
    
    def take_damage(self, amount):
        self.health = max(0, self.health - amount)

# 需要不可变的值对象 → frozen dataclass
@dataclass(frozen=True)
class Color:
    r: int
    g: int
    b: int
    
    @classmethod
    def from_hex(cls, hex_str):
        hex_str = hex_str.lstrip("#")
        return cls(
            int(hex_str[0:2], 16),
            int(hex_str[2:4], 16),
            int(hex_str[4:6], 16),
        )

RED = Color(255, 0, 0)
BLUE = Color.from_hex("#0000FF")

综合实例

from dataclasses import dataclass, field, asdict, astuple
from typing import Optional
import json

@dataclass
class Address:
    street: str
    city: str
    country: str = "China"

@dataclass
class Person:
    name: str
    age: int
    email: Optional[str] = None
    address: Optional[Address] = None
    tags: list[str] = field(default_factory=list)
    
    def __post_init__(self):
        if self.age < 0:
            raise ValueError("年龄不能为负")
    
    def to_dict(self):
        return asdict(self)
    
    def to_json(self):
        return json.dumps(asdict(self), ensure_ascii=False, indent=2)

person = Person(
    name="Alice",
    age=30,
    email="alice@example.com",
    address=Address("中关村大街", "北京"),
    tags=["developer", "python"],
)

print(person.to_json())
# {
#   "name": "Alice",
#   "age": 30,
#   "email": "alice@example.com",
#   "address": {
#     "street": "中关村大街",
#     "city": "北京",
#     "country": "China"
#   },
#   "tags": ["developer", "python"]
# }

# asdict 递归转换为字典
d = asdict(person)

# astuple 转换为元组
t = astuple(person)

本章小结dataclass 极大简化了数据类的定义,是现代 Python 的标配。用 field() 控制字段行为,用 __post_init__ 做验证和派生,用 frozen=True 创建不可变对象,用 slots=True 优化内存。在 NamedTuple 和 dataclass 之间选择时,根据是否需要元组特性和可变性来决定。