py-automapper非常详细的详解及源码分析——看完不会用你打我

一、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子类这一种情况,作者如果后续能改进的话咱们用起来也会更方便。

这也是一篇干货满满的文章,都看到这里了希望大家能点赞、评论支持下,谢谢。

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

changuncle

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值