前置知识: Python

数据类与字段默认值

3 minAdvanced2026/6/14

Python数据类与字段默认值详解:dataclass、field、默认值工厂。

概述

dataclass 是 Python 3.7 引入的装饰器,用于自动生成数据的常用方法(initrepreq 等)。相比手动编写这些方法,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=Trueunsafe_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)

自定义 hasheq

@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='')