发布于 2026-01-06 4 阅读
0

在 Python 中实现 DTO 的 7 种方法以及需要注意的事项

在 Python 中实现 DTO 的 7 种方法以及需要注意的事项

数据传输对象 (DTO)是一种数据结构,通常用于在应用程序层之间或服务之间传递数据。

Python 中最简单的 DTO 形式就是一个字典

itemdto = {
    "name": "Potion",
    "location": Location("Various"),
    "description": "Recover 20 HP",
}
Enter fullscreen mode Exit fullscreen mode

问题(包括但不限于):可变、缺乏类型、缺乏特异性。

这里我们将使用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
Enter fullscreen mode Exit fullscreen mode

如果我们想要更精确地设置 DTO 的属性,我们可以定义一个单独的

class ItemDto:
    def __init__(self, name: str, location: Location, description: str = "") -> None:
        self.name = name
        self.location = location
        self.description = description
Enter fullscreen mode Exit fullscreen mode

或者,我们可以使用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"
)
Enter fullscreen mode Exit fullscreen mode

定义良好的数据目标对象 (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",
)
Enter fullscreen mode Exit fullscreen mode
  • 生成的__repr__方法返回一个包含类名、字段名和字段表示形式的字符串。
>>> print(itemdto)
ItemDto(name='Old Rod', location=Location(name=Vermillion City), description='Fish for low-level Pokemon')
Enter fullscreen mode Exit fullscreen mode
  • 生成的__eq__方法比较包含当前实例和其他实例的字段值的类元组。
itemdto2 = ItemDto(
    name="Old Rod",
    location=Location("Vermillion City"),
    description="Fish for low-level Pokemon",
)

>>> itemdto == itemdto2
True
Enter fullscreen mode Exit fullscreen mode
  • __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
Enter fullscreen mode Exit fullscreen mode
  • 将 DTO 实例设置为不可变可能是一个好主意。可以通过设置参数frozen来实现True
@dataclass(frozen=True)
class ItemDto:
    name: str
    location: Location
    description: str = ""
Enter fullscreen mode Exit fullscreen mode
  • 不可迭代:
...: for field in itemdto:
...:     print(field)
...: 
TypeError: 'ItemDto' object is not iterable
Enter fullscreen mode Exit fullscreen mode

关于数据类的更多信息:

命名元组

  • NamedTuple是常规元组的子类
  • 在 Python 3.0 中,作为 collections 模块中的一个工厂方法引入:
from collections import namedtuple

ItemDto = namedtuple("ItemDto", ["name", "location", "description"])
Enter fullscreen mode Exit fullscreen mode
  • 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')
Enter fullscreen mode Exit fullscreen mode
  • 不可更改的
  • __repr____eq__处理
  • 可迭代的
...: for field in itemdto:
...:     print(field)
...: 
X Speed
Temporarily raise Speed in battle
Location(name=Celadon Dept. Store)
Enter fullscreen mode Exit fullscreen mode
  • 支持默认值,但这些默认值必须在任何没有默认值的字段之后定义。

关于命名元组的更多信息:

类型字典

  • 自 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'}
Enter fullscreen mode Exit fullscreen mode
  • 可变
  • __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 = ""
Enter fullscreen mode Exit fullscreen mode
  • 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')
Enter fullscreen mode Exit fullscreen mode

选择 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")
Enter fullscreen mode Exit fullscreen mode
  • 与 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")
Enter fullscreen mode Exit fullscreen mode
  • 启用(递归)数据验证:
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'>])
Enter fullscreen mode Exit fullscreen mode
  • 启用 JSON(反)序列化:
>>> PokemonDto(name="Charizard", type="Fire").json()
'{"name": "Charizard", "type": "Fire"}'
Enter fullscreen mode Exit fullscreen mode

关于 pydantic 的更多信息:

概括

如何实施 DTO 取决于多种情况,例如您是否需要:

  • 不变性
  • 支持默认值
  • 可迭代性
  • 序列化
  • 运行时类型检查
  • 性能优化
  • 其他更高级的可配置性。

这篇文章的灵感最初来源于Reddit上的这个帖子。

文章来源:https://dev.to/izabelakowal/some-ideas-on-how-to-implement-dtos-in-python-be3