如何用python-attrs将嵌套字典转为可属性访问的嵌套对象列表?
用python-attrs实现嵌套字典的"Objectify"方案
我之前刚好折腾过类似的需求,用python-attrs完全能搞定,而且你提到的循环调用attr.make_class()不仅可行,还是实现这个嵌套转换的核心思路,完美契合你所有的约束条件(保留顺序、支持嵌套、属性访问)。
核心思路拆解
我们需要一个递归处理逻辑,对不同类型的数据做针对性转换:
- 基础类型/None:直接返回,不需要转换
- 列表/元组:遍历每个元素递归转换,保留原序列类型
- 字典:用
attr.make_class()动态生成对应的attrs类,按原字典的键顺序定义类属性,同时递归处理每个值,最后实例化这个动态类
完整代码实现
首先导入必要的模块:
import attr from typing import Any
然后写递归转换函数:
def objectify(data: Any) -> Any: # 处理字符串、数字、None这类基础类型 if isinstance(data, (str, int, float, type(None))): return data # 处理列表和元组,递归转换每个元素后保留原类型 elif isinstance(data, (list, tuple)): converted_items = [objectify(item) for item in data] return tuple(converted_items) if isinstance(data, tuple) else converted_items # 处理字典,动态生成attrs类 elif isinstance(data, dict): # 按原字典的键顺序构建类字段,确保顺序保留 class_fields = {} for key, value in data.items(): # 递归处理每个值,作为字段的默认值 converted_val = objectify(value) class_fields[key] = attr.ib(default=converted_val) # 动态生成类:用字典的id做后缀避免类名冲突,order=False关闭自动比较方法 dynamic_class = attr.make_class( f"DynamicObj_{id(data)}", class_fields, order=False, kw_only=False ) # 实例化并返回 return dynamic_class(**data) # 其他未预期类型直接返回 else: return data
测试示例输入
用你给出的样例测试一下:
sample_input = { 'some_key': [ {'some_value': 12, 'foo': 'abc', 'bar': [1, 4, 8], 'baz': None}, {'some_value': 24, 'foo': 'xyz', 'bar': [12, 12], 'baz': 'this'} ], 'other_key': [1,2,3,4] } # 转换数据 result = objectify(sample_input) # 验证属性访问 print(result.some_key[0].some_value) # 输出 12 print(result.some_key[0].baz) # 输出 None print(result.other_key[-1]) # 输出 4
关键细节说明
- 顺序保留:Python 3.7+的字典本身是插入有序的,我们遍历字典键值对的顺序就是原数据的顺序,生成attrs类时字段顺序和原字典一致,所以实例的属性顺序完全保留。
- 动态类的可行性:循环/递归调用
attr.make_class()完全没问题,这个API就是为动态生成attrs类设计的。每个不同结构的字典会生成独立的动态类,用id(data)做类名后缀是为了避免不同字典(哪怕结构相同)生成同名类的冲突。 - 嵌套处理:递归函数会穿透所有层级的嵌套字典和序列,确保每个嵌套结构都被转换成可属性访问的对象。
可选优化:复用相同结构的类
如果你的数据中有大量结构相同的字典,可以加个缓存避免重复生成相同的动态类,提升性能:
from functools import lru_cache # 生成字典结构的哈希键,用于缓存 def _get_struct_key(data: dict) -> tuple: struct_items = [] for key, value in data.items(): if isinstance(value, dict): struct_items.append((key, _get_struct_key(value))) elif isinstance(value, (list, tuple)): elem_key = _get_struct_key(value[0]) if value else None struct_items.append((key, (type(value).__name__, elem_key))) else: struct_items.append((key, type(value).__name__)) return tuple(struct_items) # 缓存已生成的动态类 _class_cache = {} def objectify(data: Any) -> Any: if isinstance(data, (str, int, float, type(None))): return data elif isinstance(data, (list, tuple)): converted_items = [objectify(item) for item in data] return tuple(converted_items) if isinstance(data, tuple) else converted_items elif isinstance(data, dict): struct_key = _get_struct_key(data) # 先查缓存,没有再生成 if struct_key not in _class_cache: class_fields = {} for key, value in data.items(): converted_val = objectify(value) class_fields[key] = attr.ib(default=converted_val) _class_cache[struct_key] = attr.make_class( f"DynamicObj_{hash(struct_key)}", class_fields, order=False, kw_only=False ) return _class_cache[struct_key](**data) else: return data
关于循环调用attr.make_class()的疑问
完全可行!这是处理这种动态嵌套场景的非常自然的方式。attr.make_class()本身就支持在运行时动态创建attrs类,递归/循环调用它可以轻松处理任意深度的嵌套字典结构,只要我们按正确的逻辑处理每个层级的字段和值就行。
内容的提问来源于stack exchange,提问作者Philippe Ombredanne
相关产品推荐
相关产品推荐

