一、py-automapper简介
开发过.Net项目的工程师大部分都用过AutoMapper来进行对象映射,py-automapper就是本第三方包的Python版本。我不太确定Python版本是否覆盖了.Net版本的所有功能,但常用功能都实现了:对象映射、空值处理、属性特殊处理等。
注意:本文章动笔时使用的py-automapper=v1.2.3
安装命令:pip install py-automapper
py-automapper Github地址、py-automapper pypi地址
二、简单类型映射(非继承自BaseModel)
两图对比,我们可以得出以下结论:
- target_cls必须有__init__()且至少有一个参数,或target_cls必须继承自BaseModel(后面会解释原因)
- source_cls可以有__init__()也可以没有(图1的PersonInfo有__init__()、图2的PersonInfo没有,图1图2的public_info01、public_info02都转换成功)
- mapper.to(xxx).map()进行对象映射时不需要添加配置(public_info01、public_info02都转换成功)
- mapper.map(xxx)进行对象映射时必须添加配置(图1的public_info03转换失败、图2添加配置后public_info03转换成功)
- 图2只添加PersonInfo与PublicPersonInfo的配置即可转换成功,无需配置Address的映射关系,说明简单类型映射时只需要配置source_cls与target_cls,内部属性是类型我们不用再为其添加配置
三、dataclass的作用(非继承自BaseModel)
本图跟上一张图片的区别有两点:本图的Address()添加了@dataclass标签,内部的__init__()换成了四个属性。最终结果相同说明@dataclass为Address添加了__init__()方法,后来我查了一下资料果然不出所料:@dataclass装饰器可以帮你生成 __repr__、 __init__、__str__ 等等方法,帮助我们简化数据类的定义过程。
四、复杂类型映射(继承自BaseModel)
两图对比,我们可以得出以下结论:
- 内部属性是类型且直接或间接继承自BaseModel时,我们必须单独为内部属性的类型添加配置,不然会报错(后面会解释原因)
- 直接或间接继承BaseModel也会像@dataclass一样帮你生成 __repr__、 __init__、__str__ 等等方法,帮助我们简化数据类的定义过程
五、fields_mapping自定义属性映射关系
六、深度分析遗留问题1
问题1:target_cls必须有__init__()且至少有一个参数,或target_cls必须继承自BaseModel
def map(self, obj: object, *,
skip_none_values: bool = False,
fields_mapping: FieldsMap = None,
use_deepcopy: bool = True,
) -> T: # type: ignore [type-var]
"""Produces output object mapped from source object and custom arguments
Args:
obj (object): Source object to map to `target class`.
skip_none_values (bool, optional): Skip None values when creating `target class` obj. Defaults to False.
fields_mapping (FieldsMap, optional): Custom mapping.
Specify dictionary in format {"field_name": value_object}. Defaults to None.
use_deepcopy (bool, optional): Apply deepcopy to all child objects when copy from source to target object.
Defaults to True.
Raises:
MappingError: No `target class` specified to be mapped into.
Register mappings using `mapped.add(...)` or specify `target class` using `mapper.to(target_cls).map()`.
CircularReferenceError: Circular references in `source class` object are not allowed yet.
Returns:
T: instance of `target class` with mapped values from `source class` or custom `fields_mapping` dictionary.
"""
obj_type = type(obj)
if obj_type not in self._mappings:
raise MappingError(f"Missing mapping type for input type {obj_type}")
obj_type_prefix = f"{obj_type.__name__}."
target_cls, target_cls_field_mappings = self._mappings[obj_type]
common_fields_mapping = fields_mapping
if target_cls_field_mappings:
# transform mapping if it's from source class field
common_fields_mapping = {
target_obj_field: getattr(obj, source_field[len(obj_type_prefix) :])
if isinstance(source_field, str)
and source_field.startswith(obj_type_prefix)
else source_field
for target_obj_field, source_field in target_cls_field_mappings.items()
}
if fields_mapping:
common_fields_mapping = {
**common_fields_mapping,
**fields_mapping,
} # merge two dict into one, fields_mapping has priority
return self._map_common(obj, target_cls, set(),
skip_none_values=skip_none_values, custom_mapping=common_fields_mapping, use_deepcopy=use_deepcopy,
)
对象映射的入口是map()方法,上面就是map()的源代码,map()最后调用了_map_common(),_map_common()又调用了_get_fields(),下面先分析_get_fields(),再说_map_common()。
根据_get_fields()截图中控制台打印的内容可知:
- self._class_specs中包含pydantic.main.BaseModel
- self._classifier_specs包含__init_method_classifier__
- classifier(target_cls)返回构造函数的参数
由此三点就解释了问题1的原因,【target_cls必须有__init__()且至少有一个参数】对应263~265行,【target_cls必须继承自BaseModel】对应259~261行,如果这两条都不满足的话直接raise MappingError,因此两条中至少要满足1条。
_get_fields()拿到target_cls的所有属性后,回到_map_common()循环从source_cls的对象中获取对应的属性值,最后cast()拿到target_cls的对象。
七、深度分析遗留问题2
问题2:内部属性是类型且直接或间接继承自BaseModel时,我们必须单独为其添加配置,不然会报错
经过调试发现不添加配置时_map_subobject()报错了,_map_common()截图的340行调用了_map_subobject(),即深度拷贝时获取PersonInfo.address的属性值报错了。
根据_map_subobject()截图中控制台打印的内容可知:
- 当前obj是Address类型
- 由于Address继承自BaseModel,导致obj.__iter__()被重写,_is_sequence(obj)判断为true
- obj是Address类型不是dict,所以290行判断失败进入298行,但实际上Address是不能用[...]实参来创建对象的
- 常见的可迭代对象:tuple、list、set,这些可以用一个[...]实参来创建对象(如图所示)
至此我们可以得出结论:内部属性是类型且直接或间接继承自BaseModel时,我们必须单独为其添加配置,不然会报错。我认为这不能算是问题,只能说是1.2.3版本中处理_is_sequence()时少判断了BaseModel子类这一种情况,作者如果后续能改进的话咱们用起来也会更方便。
这也是一篇干货满满的文章,都看到这里了希望大家能点赞、评论支持下,谢谢。