在 Python 中实现 DTO 的 7 种方法以及需要注意的事项
数据传输对象 (DTO)是一种数据结构,通常用于在应用程序层之间或服务之间传递数据。
Python 中最简单的 DTO 形式就是一个字典:
itemdto = {
"name": "Potion",
"location": Location("Various"),
"description": "Recover 20 HP",
}
问题(包括但不限于):可变、缺乏类型、缺乏特异性。
这里我们将使用Location类,在实际场景中,它可能是 ORM 管理的模型类:
class Location:
def __init__(self, name: str) -> None:
self.name = name
def __repr__(self) -> str:
return f"Location(name={self.name})"
def __eq__(self, other: Location) -> bool:
return self.name == other.name
如果我们想要更精确地设置 DTO 的属性,我们可以定义一个单独的类:
class ItemDto:
def __init__(self, name: str, location: Location, description: str = "") -> None:
self.name = name
self.location = location
self.description = description
或者,我们可以使用kwargs一个.get()带有可选参数的方法:
class ItemDto:
def __init__(self, **kwargs) -> None:
self.name = kwargs["name"]
self.location = kwargs["location"]
self.description = kwargs.get("description", "")
itemdto = ItemDto(
name="Super Potion",
location=Location("Various"),
description="Recover 70 HP"
)
定义良好的数据目标对象 (DTO) 可以带来更多好处,例如简化序列化和验证过程。以下是一些利用 Python 标准库和第三方包的不同特性来创建更优质 DTO 的示例。
标准库解决方案
数据类
-
已添加到 Python 3.7(后来又移植到 Python 3.6)
-
使用
@dataclass装饰器创建 -
默认情况下,自动添加双下划线方法
__init__,__repr__并且__eq__ -
__init__该方法将所有字段作为方法参数,并将它们的值设置为同名的实例属性:
from dataclasses import dataclass
@dataclass
class ItemDto:
name: str
location: Location
description: str = ""
# support both positional and keyword args
itemdto = ItemDto(
name="Old Rod",
location=Location("Vermillion City"),
description="Fish for low-level Pokemon",
)
- 生成的
__repr__方法返回一个包含类名、字段名和字段表示形式的字符串。
>>> print(itemdto)
ItemDto(name='Old Rod', location=Location(name=Vermillion City), description='Fish for low-level Pokemon')
- 生成的
__eq__方法比较包含当前实例和其他实例的字段值的类元组。
itemdto2 = ItemDto(
name="Old Rod",
location=Location("Vermillion City"),
description="Fish for low-level Pokemon",
)
>>> itemdto == itemdto2
True
__eq__该方法的效果与我们显式声明该方法的效果相同:
def __eq__(self, other):
if other.__class__ is self.__class__:
return (self.name, self.location, self.description) == (other.name, other.location, other.description)
return NotImplemented
- 将 DTO 实例设置为不可变可能是一个好主意。可以通过设置参数
frozen来实现True:
@dataclass(frozen=True)
class ItemDto:
name: str
location: Location
description: str = ""
- 不可迭代:
...: for field in itemdto:
...: print(field)
...:
TypeError: 'ItemDto' object is not iterable
关于数据类的更多信息:
- https://docs.python.org/3/library/dataclasses.html#module-dataclasses
- https://realpython.com/python-data-classes/
命名元组
NamedTuple是常规元组的子类- 在 Python 3.0 中,作为 collections 模块中的一个工厂方法引入:
from collections import namedtuple
ItemDto = namedtuple("ItemDto", ["name", "location", "description"])
- Python 3.5 中新增了 typed 模块的类型化版本,后来在 Python 3.6 中又通过变量注解语法进行了增强:
from typing import NamedTuple
class ItemDto(NamedTuple):
name: str
location: Location
description: str = ""
# support both positional and keyword args
itemdto = ItemDto(
"X Speed", "Temporarily raise Speed in battle", Location("Celadon Dept. Store")
)
>>> print(itemdto)
ItemDto(name='X Speed', location='Temporarily raise Speed in battle', description='Celadon Dept. Store')
- 不可更改的
__repr__并__eq__处理- 可迭代的
...: for field in itemdto:
...: print(field)
...:
X Speed
Temporarily raise Speed in battle
Location(name=Celadon Dept. Store)
- 支持默认值,但这些默认值必须在任何没有默认值的字段之后定义。
关于命名元组的更多信息:
类型字典
- 自 Python 3.8 起可用:
from typing import TypedDict
class ItemDto(TypedDict):
name: str
location: Location
description: str
itemdto = ItemDto(
name="Escape Rope,",
location=Location("Various"),
description="Teleport to last visited Pokemon Center",
)
>>> print(itemdto)
{'name': 'Escape Rope,', 'location': Location(name=Various), 'description': 'Teleport to last visited Pokemon Center'}
- 可变
__repr__并__eq__处理- 以字典的方式进行迭代
- 不支持默认值
- 可以为现有词典提供输入法
- 因为它们毕竟仍然是字典,所以可以直接序列化为 JSON 数据结构(尽管在本例中,我们应该为该类提供一个自定义编码器
Location)。
关于 TypedDicts 的更多信息:
第三方软件包
属性
- pytest 是一个依赖项,所以你的项目中可能已经包含了它。
- 事实上,与数据类类似,attrs 库是设计数据类的基础:
import attr
@attr.s(frozen=True)
class ItemDto:
name: str = attr.ib()
location: Location = attr.ib()
description: str = attr.ib(default="")
# also, the dataclasses syntax!
@attr.dataclass(frozen=True)
class ItemDto:
name: str
location: Location
description: str = ""
- attrs 在数据类提供的功能基础上提供了一些额外的功能,例如运行时验证和内存优化(槽类)。
以下是一个运行时验证的示例:
@attr.s(frozen=True)
class PokemonDto:
name: str = attr.ib()
type: str = attr.ib(
validator=attr.validators.in_(
[
"Fire",
"Water",
"Electric",
"Poison", # ...
]
)
)
>>> PokemonDto("Charmander", "Fire")
PokemonDto(name='Charmander', type='Fire')
>>> PokemonDto("Charmander", "Gyarados")
ValueError: 'type' must be in ['Fire', 'Water', 'Electric', 'Poison'] (got 'Gyarados')
选择 attrs 还是 dataclasses,最终取决于您的具体用例以及您是否能够在项目中使用第三方包。
关于属性的更多信息:
吡啶酮
-
FastAPI 使用 pydantic 进行模式定义和数据验证
-
pydantic 在运行时强制执行类型提示。
-
创建 Pydantic 模型的推荐方法是继承类
pydantic.BaseModel,因此所有模型都会继承一些方法:
from pydantic import BaseModel
class PokemonDto(BaseModel):
name: str
type: str
class Config:
allow_mutation = False
# enforced keyword arguments in case of BaseModel subclass
pokemondto = PokemonDto(name="Charizard", type="Fire")
- 与 attrs 类似,pydantic 也支持原生 Python 数据类:
import pydantic
@pydantic.dataclasses.dataclass(frozen=True)
class PokemonDto:
name: str
type: str
# in this case positional args are allowed
PokemonDto("Charizard", "Fire")
- 启用(递归)数据验证:
from enum import Enum
from pydantic import BaseModel
class TypeEnum(str, Enum):
fire = "Fire"
water = "Water"
electric = "Electric"
poison = "Poison"
# ...
class PokemonDto(pydantic.BaseModel):
name: str
type: TypeEnum
class Config:
allow_mutation = False
>>> PokemonDto(name="Charizard", type="Fire")
PokemonDto(name='Charizard', type=<TypeEnum.fire: 'Fire'>)
>>> PokemonDto(name="Charizard", type="Charmeleon")
ValidationError: 1 validation error for PokemonDto
type
value is not a valid enumeration member; permitted: 'Fire', 'Water', 'Electric', 'Poison' (type=type_error.enum; enum_values=[<TypeEnum.fire: 'Fire'>, <TypeEnum.water: 'Water'>, <TypeEnum.electric: 'Electric'>, <TypeEnum.poison: 'Poison'>])
- 启用 JSON(反)序列化:
>>> PokemonDto(name="Charizard", type="Fire").json()
'{"name": "Charizard", "type": "Fire"}'
关于 pydantic 的更多信息:
概括
如何实施 DTO 取决于多种情况,例如您是否需要:
- 不变性
- 支持默认值
- 可迭代性
- 序列化
- 运行时类型检查
- 性能优化
- 其他更高级的可配置性。
这篇文章的灵感最初来源于Reddit上的这个帖子。
文章来源:https://dev.to/izabelakowal/some-ideas-on-how-to-implement-dtos-in-python-be3