数据传输对象(DTO)的主要目标是简化应用程序不同层之间的通信,尤其是在通过各种边界接口(如Web服务、REST API、消息代理或其他远程交互机制)传输数据时。在与其他系统交换信息时,关键是要最小化不必要的开销,例如冗余的序列化/反序列化,并确保清晰的数据结构,以表示发送者和接收者之间的特定合同。
在本文中,我打算探讨Python在实现DTO方面提供的能力。从内置工具开始,扩展到专门的库。在核心功能中,我希望强调类型和数据验证、对象创建以及转换为字典的能力。
基于Python类的DTO
让我们考虑一个基于Python类的DTO的示例。假设我们有一个用户模型,其中包括名字和姓氏:
class UserDTO:
def __init__(self, **kwargs):
self.first_name = kwargs.get("first_name")
self.last_name = kwargs.get("last_name")
self.validate_lastname()
def validate_lastname(self):
if len(self.last_name) <= 2:
raise ValueError("姓氏长度必须大于2")
def to_dict(self):
return self.__dict__
@classmethod
def from_dict(cls, dict_obj):
return cls(**dict_obj)
我们在DTO类中实现了用于创建类实例、将数据转换为字典以及添加验证逻辑的方法。现在,让我们看看如何使用它:
>>> user_dto = UserDTO.from_dict({'first_name': 'John', 'last_name': 'Doe'})
>>> user_dto.to_dict()
{'first_name': 'John', 'last_name': 'Doe'}
>>> user_dto = UserDTO.from_dict({'first_name': 'John', 'last_name': 'Do'})
ValueError: 姓氏长度必须大于2
这只是一个高度简化的示例。这种方法允许您实现各种功能。唯一的缺点是您需要手动定义所有内容,即使使用继承,也可能有大量的代码。
NamedTuple
创建Python中的DTO的另一种方法是使用NamedTuple。
NamedTuple是Python标准库中的一个类(在Python 3.6中引入),表示具有命名属性的不可变元组。它是collections模块中namedtuple类的一种类型化且更易读的版本。
我们可以基于NamedTuple创建一个DTO,其中包含上述示例中用户的名字和姓氏:
from typing import NamedTuple
class UserDTO(NamedTuple):
first_name: str
last_name: str
现在,我们可以像这样创建UserDTO对象,并将对象转换为字典,以及通过字典创建对象:
>>> user_dto = UserDTO(**{'first_name': 'John', 'last_name': 'Doe'})
>>> user_dto.first_name
'John'
>>> user_dto
UserDTO(first_name='John', last_name='Doe')
>>> user_dto._asdict()
{'first_name': 'John', 'last_name': 'Doe'}
>>> user_dto.first_name = 'Bill'
AttributeError: 无法设置属性
它没有内置的类型和数据验证,但提供了更紧凑的定义和更易读的格式。它还是不可变的,在操作过程中提供了更多的安全性。只能传递定义的参数作为输入,并且有一个_asdict
方法可将其转换为字典。
详细信息请参见这里。
TypedDict
在Python中创建DTO对象的另一个选项是使用TypedDict,它从3.8版本开始引入。这种数据类型允许您创建带有固定键集和值类型注释的字典。
当您需要使用具有特定键集的字典时,TypedDict是一个很好的选择。
要创建对象,您需要从typing模块导入TypedDict数据类型。让我们为用户模型创建一个TypedDict:
from typing import TypedDict
class UserDTO(TypedDict):
first_name: str
last_name: str
在这个示例中,我们定义了UserDTO类,它是TypedDict的子类。我们可以创建一个UserDTO对象并填充数据:
>>> user_dto = UserDTO(**{first_name: 'John', last_name: 'Doe'})
>>> user_dto
{first_name: 'John', last_name: 'Doe'}
>>> type(user_dto)
<class 'dict'>
我们可以使用它来定义具有固定键集和值类型注释的字典。这使得代码更易读,更可预测。此外,TypedDict提供了使用诸如keys()和values()等字典方法的能力,在某些情况下可能很有用。
详细信息请参见这里。
Dataclass
Dataclass是一个装饰器,提供了一种简单的方式来创建用于存储数据的类。Dataclass使用类型注释来定义字段,然后生成创建和使用该类对象所需的所有必要方法。
要使用dataclass创建DTO,您需要添加dataclass
装饰器并使用类型注释定义字段。例如,我们可以像这样使用dataclass创建用户模型的DTO:
from dataclasses import asdict, dataclass
@dataclass
class UserDTO:
first_name: str
last_name: str = ''
def __post_init__(self):
self.validate_lastname()
def validate_lastname(self):
if len(self.last_name) <= 2:
raise ValueError("姓氏长度必须大于2")
现在,我们可以轻松地创建UserDTO
对象,将它们转换为字典,并根据字典创建新对象:
>>> user_dto = UserDTO(**{'first_name': 'John', 'last_name': 'Doe'})
>>> user_dto
UserDTO(first_name='John', last_name='Doe')
>>> asdict(user_dto)
{'first_name': 'John', 'last_name': 'Doe'}
>>> user_dto = UserDTO(**{'first_name': 'John', 'last_name': 'Do'})
ValueError: 姓氏长度必须大于2
要创建不可变对象,您可以将参数frozen=True
传递给装饰器。有一个asdict
方法可用于转换为字典。此外,您可以实现验证方法并使用默认值。
总的来说,dataclasses比普通类更紧凑,比前面讨论的选项功能更强大。
详细信息请参见这里。
Attr
创建DTO的另一种方法是使用attr模块。它与dataclass类似,但功能更强大,提供更紧凑的描述。该库可以使用命令pip install attrs进行安装。
import attr
@attr.s
class UserDTO:
first_name: str = attr.ib(default="John", validator=attr.validators.instance_of(str))
last_name: str = attr.ib(default="Doe", validator=attr.validators.instance_of(str))
在这里,我们使用装饰器定义了一个带有属性first_name
和last_name
的DTO类,同时设置了默认值和验证。
>>> user_dto = UserDTO(**{'first_name': 'John', 'last_name': 'Doe'})
>>> user_dto
UserDTO(first_name='John', last_name='Doe')
>>> user_dto = UserDTO()
>>> user_dto
UserDTO(first_name='John', last_name='Doe')
>>> user_dto = UserDTO(**{'first_name': 1, 'last_name': 'Doe'})
TypeError: ("'first_name' must be <class 'str'>...
通过这种方式,attrs模块提供了定义DTO类的强大而灵活的工具,如验证、默认值和转换。DTO对象也可以通过装饰器属性frozen=True变为不可变。还可以通过define装饰器进行初始化。
详细信息请参见这里。
Pydantic
Pydantic库是Python中用于数据验证和数据转换的工具。它利用类型注释来定义数据模式,并将JSON数据转换为Python对象。
Pydantic用于方便地处理来自Web请求、配置文件、数据库和其他需要数据验证和转换的场景。
可以使用命令pip install pydantic来安装它。
from pydantic import BaseModel, Field, field_validator
class UserDTO(BaseModel):
first_name: str
last_name: str = Field(min_length=2, alias="lastName")
age: int = Field(lt=100, description="年龄必须是正整数")
@field_validator("age")
def validate_age(cls, value):
if value < 18:
raise ValueError("年龄必须至少为18岁")
return value
在这个示例中,我们定义了一个UserDTO模型,其中包含了字符串长度和最大年龄的基本验证。我们还指定了last_name属性的数据将通过参数lastName传递。
此外,作为示例,还演示了一个用于最小年龄的自定义验证器。
>>> user_dto = UserDTO(**{'first_name': 'John', 'lastName': 'Doe', 'age': 31})
>>> user_dto
UserDTO(first_name='John', last_name='Doe', age=31)
>>> user_dto.model_dump()
{'first_name': 'John', 'last_name': 'Doe', 'age': 31}
>>> user_dto.model_dump_json()
'{"first_name":"John","last_name":"Doe","age":31}'
>>> user_dto = UserDTO(**{'first_name': 'John', 'lastName': 'D', 'age': 3})
pydantic_core._pydantic_core.ValidationError: 2 validation errors for UserDTO
lastName
String should have at least 2 characters [type=string_too_short, input_value='D', input_type=str]
age
Value error, Age must be at least 18 [type=value_error, input_value=3, input_type=int]
Pydantic是一种多功能的工具。它是FastAPI中默认的数据模式和验证工具。它通过内置方法简化了对象到JSON格式的序列化和反序列化,提供了更易读的运行时提示。
详细信息请参见这里。
总结
在本文中,我介绍了在Python中实现DTO的不同方法,从简单的方法到更复杂的方法。选择在项目中使用哪种方法取决于各种因素。
这包括您项目中的Python版本以及是否有能力安装新的依赖项。还取决于您是否计划使用验证、转换,或者是否仅需要简单的类型注释。
我希望这篇文章能帮助那些在寻找适用于其项目的Python中实现DTO的合适方法的人们。