Python 的 dataclasses 模块让数据类的定义变得非常简洁,而 dataclasses.asdict() 函数可以把实例递归地转换为字典,方便后续做 JSON 序列化、日志输出或缓存存储。然而当数据类字段中包含自定义类型时,事情往往没有想象中顺利,常见的结果是一个 TypeError: Object of type XXX is not JSON serializable,或者字典里的字段根本不是预期的形态。本文将深入分析 asdict() 的工作原理,并给出几种让自定义类与之兼容并正确序列化的实践方案。

一、理解 asdict() 的递归处理机制
先看一个最简单的例子,定义两个数据类并嵌套使用:
from dataclasses import dataclass, asdict
@dataclass
class Address:
city: str
street: str
@dataclass
class Person:
name: str
age: int
address: Address
p = Person("张三", 30, Address("北京", "长安街"))
d = asdict(p)
print(d)
# {'name': '张三', 'age': 30, 'address': {'city': '北京', 'street': '长安街'}}
可以看到,asdict() 对嵌套的 dataclass 实例做了递归展开,最终输出一个纯粹的字典结构。它的内部逻辑大致是:如果字段值是 dataclass 实例,就递归调用 asdict();如果是列表、元组或字典等容器,就遍历其中的元素继续递归处理;如果是枚举,会取其值;其他类型则原样返回。
这个机制的关键点在于:只有被 @dataclass 装饰的类才会被递归展开,普通类的实例会被原样放进结果字典中。例如下面这个自定义类:
class Money:
def __init__(self, amount, currency):
self.amount = amount
self.currency = currency
@dataclass
class Order:
id: int
price: Money
o = Order(1, Money(99, "CNY"))
print(asdict(o))
# {'id': 1, 'price': <__main__.Money object at 0x...>}
Money 不是 dataclass,所以 asdict() 只是把它原样保留。此时如果直接执行 json.dumps(asdict(o)),就会抛出 TypeError。理解了这个机制,后面的解决方案就有了明确的切入点:要么让自定义类本身可被识别,要么在序列化阶段提供转换规则。
二、方案一:把自定义类改造成 dataclass 或实现转换协议
最直接的办法是把所有参与嵌套的自定义类都声明为 dataclass。这样 asdict() 会自动递归处理它们,无需额外代码。对于数据承载型的类(比如上面的 Money),这是最推荐的改造方式:
from dataclasses import dataclass, asdict
@dataclass
class Money:
amount: float
currency: str
@dataclass
class Order:
id: int
price: Money
o = Order(1, Money(99, "CNY"))
print(asdict(o))
# {'id': 1, 'price': {'amount': 99.0, 'currency': 'CNY'}}
但如果自定义类来自第三方库,无法修改其源码,可以采用另一种思路:实现一个通用的转换函数,在调用 asdict() 之前先包装处理。常见做法是定义一个 to_dict 辅助函数,对无法识别的类型调用其自定义的转换方法:
from dataclasses import is_dataclass, fields, asdict
def deep_asdict(obj):
# 是 dataclass 就递归展开
if is_dataclass(obj) and not isinstance(obj, type):
result = {}
for f in fields(obj):
result[f.name] = deep_asdict(getattr(obj, f.name))
return result
# 是常见容器就逐项处理
elif isinstance(obj, list):
return [deep_asdict(item) for item in obj]
elif isinstance(obj, tuple):
return tuple(deep_asdict(item) for item in obj)
elif isinstance(obj, dict):
return {key: deep_asdict(value) for key, value in obj.items()}
# 自定义类型调用其 as_dict 方法
elif hasattr(obj, "as_dict"):
return obj.as_dict()
return obj
这种方式的优点是灵活,可以覆盖任意第三方类型,只要对方约定了 as_dict 接口。缺点是需要自己维护一套递归逻辑,容器类型的处理容易遗漏(例如 set、deque 等),在生产环境使用前建议补充完整的类型分支。
三、方案二:用 __post_init__ 和 default 参数处理特殊字段
有些字段本身不是自定义类,而是 Python 内置但不可 JSON 序列化的类型,比如 datetime、Decimal、UUID 等。asdict() 会把它们原样放入字典,问题出现在 json.dumps() 阶段。处理这类场景有两种思路。
第一种是利用 __post_init__ 在实例化时就把字段转成字符串,这样从源头保证了序列化安全:
from dataclasses import dataclass, field, asdict
from datetime import datetime
@dataclass
class Event:
name: str
created_at: str = field(default="")
def __post_init__(self):
if not self.created_at:
self.created_at = datetime.now().isoformat()
e = Event("登录")
print(asdict(e))
# {'name': '登录', 'created_at': '2023-05-20T10:30:00.123456'}
第二种思路是保留原始类型,只在序列化时提供转换规则。json.dumps() 支持 default 参数,遇到无法序列化的对象时会调用它。把 asdict() 与 default 结合,既保留了字段的类型信息,又能顺利输出 JSON:
import json
from dataclasses import dataclass, asdict
from datetime import datetime
from decimal import Decimal
from uuid import UUID
@dataclass
class Trade:
order_id: UUID
amount: Decimal
created_at: datetime
def json_default(obj):
if isinstance(obj, Decimal):
return float(obj)
if isinstance(obj, (datetime, UUID)):
return str(obj)
raise TypeError(f"无法序列化的类型: {type(obj)}")
t = Trade(UUID("12345678-1234-5678-1234-567812345678"), Decimal("10.5"), datetime.now())
print(json.dumps(asdict(t), default=json_default, ensure_ascii=False))
两种方式各有适用场景:如果数据类专门用于对外输出,推荐在 __post_init__ 中直接转换,简化下游逻辑;如果数据类还要参与内部计算(比如 Decimal 需要保留精度做运算),则应保留原类型,把转换推迟到序列化边界处统一处理。
四、常见陷阱与性能优化建议
使用 asdict() 时有几个容易踩到的坑需要特别注意。
第一个坑是深拷贝行为。asdict() 会递归复制所有嵌套结构,包括列表和字典中的每个元素。如果数据类里持有大数组,频繁调用 asdict() 会带来明显的性能开销。官方文档也提醒过,这个函数本质上执行的是深拷贝。在性能敏感的场景(例如每条日志都序列化一次),可以考虑只用 vars() 或者缓存转换结果。
第二个坑是递归引用。如果数据类字段之间存在循环引用,asdict() 会无限递归直到触发 RecursionError。遇到这类结构需要在转换前打断引用,或者使用支持引用检测的序列化库。
from dataclasses import dataclass, field
@dataclass
class Node:
name: str
children: list = field(default_factory=list)
# 循环引用示例:child.parent = parent 会导致 asdict 无限递归
第三个坑是可变默认值。虽然与 asdict() 本身无关,但很多序列化错误其实源于数据类定义阶段。务必使用 field(default_factory=list) 而不是直接写 list 字面量作为默认值,否则所有实例会共享同一个列表对象,序列化结果也会随之被意外修改。
综合来看,让自定义类与 dataclasses.asdict() 兼容的核心思路是:数据型类尽量声明为 dataclass 以享受自动递归;不可变的特殊类型通过 default 钩子在 JSON 边界处转换;第三方对象用通用递归函数配合约定接口处理;同时留意深拷贝与循环引用带来的性能和稳定性风险。掌握这些技巧后,数据类的序列化将变得稳定且可预期。
dataclasses.asdict()Python序列化自定义类修改时间:2026-09-02 17:11:13