数据类与字段默认值
00:00
Python数据类与字段默认值详解:dataclass、field、默认值工厂。
概述
dataclass 是 Python 3.7 引入的装饰器,用于自动生成数据类的常用方法(init、repr、eq 等)。相比手动编写这些方法,dataclass 减少了样板代码,同时支持字段默认值、默认值工厂、不可变对象等特性。理解字段默认值的机制对于正确使用 dataclass 至关重要。
基础概念
dataclass 基本用法
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p) # Point(x=1.0, y=2.0)
# 自动生成了 __init__、__repr__、__eq__
字段默认值
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = False
# 使用默认值
c = Config()
print(c) # Config(host='localhost', port=8080, debug=False)
# 覆盖默认值
c2 = Config(host="0.0.0.0", port=3000)
可变默认值陷阱
# 错误:使用可变对象作为默认值
@dataclass
class Bad:
items: list = [] # 所有实例共享同一个列表!
b1 = Bad()
b2 = Bad()
b1.items.append(1)
print(b2.items) # [1] — 被污染了!
# 正确:使用 default_factory
@dataclass
class Good:
items: list = field(default_factory=list)
g1 = Good()
g2 = Good()
g1.items.append(1)
print(g2.items) # [] — 独立的列表
快速上手
field 函数
from dataclasses import dataclass, field
@dataclass
class User:
name: str
age: int = 0
tags: list = field(default_factory=list)
email: str = field(default="", repr=False) # 不在 repr 中显示
id: int = field(init=False, default=0) # 不参与 __init__,有默认值
u = User("Alice", 30)
print(u) # User(name='Alice', age=30, tags=[], id=0)
常用 field 参数
| 参数 | 说明 |
|---|---|
| default | 字段默认值 |
| default_factory | 默认值工厂函数,每次创建新实例时调用 |
| init | 是否包含在 init 中(默认 True) |
| repr | 是否包含在 repr 中(默认 True) |
| compare | 是否参与比较(默认 True) |
| hash | 是否参与哈希计算(默认跟随 compare) |
| metadata | 自定义元数据字典 |
详细用法
default_factory 详解
default_factory 接受一个无参可调用对象,每次创建实例时调用它生成新的默认值:
import uuid
from datetime import datetime
@dataclass
class Record:
# 使用函数生成唯一 ID
id: str = field(default_factory=lambda: uuid.uuid4().hex[:8])
# 使用函数生成创建时间
created_at: datetime = field(default_factory=datetime.now)
# 使用 lambda 创建字典
metadata: dict = field(default_factory=dict)
# 使用自定义工厂函数
tags: list = field(default_factory=list)
r = Record()
print(r.id) # 随机 8 位 ID
print(r.created_at) # 当前时间
print(r.metadata) # 空字典
自定义 default_factory
def create_config():
"""自定义工厂函数"""
return {
"timeout": 30,
"retries": 3,
"endpoint": "https://api.example.com",
}
@dataclass
class Service:
name: str
config: dict = field(default_factory=create_config)
s = Service("my-service")
print(s.config) # {'timeout': 30, 'retries': 3, 'endpoint': 'https://api.example.com'}
不可变数据类
@dataclass(frozen=True)
class Coordinate:
"""不可变的坐标类"""
latitude: float
longitude: float
c = Coordinate(39.9, 116.4)
# c.latitude = 40.0 # FrozenInstanceError: 不能修改
# 不可变对象可以用作字典键或集合元素
locations = {c: "北京"}
继承与字段顺序
@dataclass
class Base:
x: int = 0
y: int = 0
@dataclass
class Derived(Base):
z: int = 0
# 注意:有默认值的字段不能出现在无默认值字段之前
d = Derived(x=1, y=2, z=3)
print(d) # Derived(x=1, y=2, z=3)
post_init 初始化后处理
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
def __post_init__(self):
"""在 __init__ 之后自动调用"""
self.area = self.width * self.height
r = Rectangle(10, 5)
print(r.area) # 50
计算字段
@dataclass
class Person:
first_name: str
last_name: str
full_name: str = field(init=False)
def __post_init__(self):
self.full_name = f"{self.first_name} {self.last_name}"
p = Person("Alice", "Smith")
print(p.full_name) # Alice Smith
常见场景
场景一:配置类
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class DatabaseConfig:
host: str = "localhost"
port: int = 5432
database: str = "mydb"
username: str = "postgres"
password: str = field(default="", repr=False) # 不在 repr 中显示密码
pool_size: int = 5
options: dict = field(default_factory=dict)
@property
def url(self):
return f"postgresql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"
config = DatabaseConfig(database="production")
print(config.url)
场景二:API 响应模型
from dataclasses import dataclass, field
from typing import List, Optional
@dataclass
class UserResponse:
id: int
name: str
email: str
avatar: str = ""
roles: List[str] = field(default_factory=list)
is_active: bool = True
@classmethod
def from_dict(cls, data: dict):
"""从字典创建实例"""
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
# 使用
response = UserResponse.from_dict({
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"extra_field": "ignored", # 被过滤掉
})
场景三:嵌套数据类
@dataclass
class Address:
city: str
street: str = ""
zipcode: str = ""
@dataclass
class Employee:
name: str
age: int
address: Address = field(default_factory=lambda: Address(city="未知"))
# 使用
emp = Employee("Alice", 30, Address("北京", "长安街", "100000"))
print(emp.address.city) # 北京
注意事项
- 有默认值的字段必须放在无默认值的字段之后,否则会报错
- 可变默认值必须使用
field(default_factory=...)而非直接赋值 frozen=True的数据类不可修改,但其中的可变属性(如列表)仍可修改- dataclass 不会自动生成
__hash__,除非设置frozen=True或unsafe_hash=True - 继承时,父类的字段顺序保留,子类字段追加在后面
field(init=False)的字段不参与构造函数,但可以通过__post_init__初始化
进阶用法
asdict 和 astuple
from dataclasses import dataclass, asdict, astuple
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(asdict(p)) # {'x': 1.0, 'y': 2.0}
print(astuple(p)) # (1.0, 2.0)
replace 创建副本
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Config:
host: str = "localhost"
port: int = 8080
default_config = Config()
# 创建修改了部分字段的副本
prod_config = replace(default_config, host="0.0.0.0", port=3000)
print(prod_config) # Config(host='0.0.0.0', port=3000)
自定义 hash 和 eq
@dataclass
class User:
id: int
name: str
email: str = field(compare=False) # 不参与比较
# 只有 id 参与哈希和比较
def __hash__(self):
return hash(self.id)
u1 = User(1, "Alice", "alice@test.com")
u2 = User(1, "Alice", "alice@other.com")
print(u1 == u2) # True — email 不参与比较
使用 KW_ONLY 强制关键字参数
from dataclasses import dataclass, KW_ONLY
@dataclass
class Point:
x: float
_: KW_ONLY # 之后的字段必须使用关键字参数
y: float = 0.0
z: float = 0.0
# p = Point(1.0, 2.0) # 错误:y 和 z 必须用关键字
p = Point(1.0, y=2.0, z=3.0) # 正确
dataclass 与 Pydantic 配合
from dataclasses import dataclass
from pydantic import TypeAdapter
@dataclass
class User:
name: str
age: int
email: str = ""
# 使用 Pydantic 进行验证
adapter = TypeAdapter(User)
user = adapter.validate_python({"name": "Alice", "age": 30})
print(user) # User(name='Alice', age=30, email='')