第 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 | 是否参与 hash | None |
init | 是否包含在 init 中 | True |
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
| 特性 | NamedTuple | dataclass | frozen dataclass |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 不可变 |
| 继承自 | tuple | object | object |
| 索引访问 | 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 之间选择时,根据是否需要元组特性和可变性来决定。