Основной целью DTO является упрощение коммуникации между слоями приложения, особенно при передаче данных через различные граничные интерфейсы, такие как веб-сервисы, REST API, брокеры сообщений или другие механизмы удаленного взаимодействия. На пути к обмену информацией с другими системами, важно минимизировать лишние расходы, такие как избыточное сериализация/десериализация, а также обеспечить четкую структуру данных, представляющую определенный контракт между отправителем и получателем.
В этой статье я хочу рассмотреть какие возможности есть у Python для реализации DTO. Начиная от встроенных инструментов, заканчивая специальными библиотеками.
Из основной функциональности хочу выделить валидацию типов и данных, создание объекта и выгрузку в словарь.
DTO на основе класса Python
Рассмотрим пример DTO на основе класса Python. Представим, что у нас есть модель пользователя, которая содержит имя и фамилию:
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("last_name length must be more then 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: last_name length must be more then 2
Это максимально упрощенный пример. Таким образом можно реализовать любую функциональность. Единственный минус - нужно всё описывать руками и даже используя наследование будет много кода.
NamedTuple
Другой способ создания DTO в Python - использование NamedTuple.
NamedTuple - это класс из стандартной библиотеки Python(начиная с версии Python 3.6), который представляет собой неизменяемый кортеж с доступом к свойствам по имени. Это типизированная и более читабельная версия класса namedtuple из модуля сollections.
Мы можем создать DTO на основе NamedTuple, содержащий имя и фамилию пользователя из примера с использованием классов:
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: can't set attribute
Встроенной валидации типов и данных нет. Но зато из коробки более компактное определение и читабельный вид. Так же является неизменяемым что дает больше безопасности при работе. На вход можно передавать только те аргументы которые определены, есть метод _asdict для преобразования в словарь.
Подробнее тут.
TypedDict
Еще одним вариантом для создания объектов DTO в Python является использование TypedDict, который добавлен в язык начиная с версии 3.8. Этот тип данных позволяет создавать словари с фиксированным набором ключей и аннотациями типов значений. Такой подход делает TypedDict хорошим выбором для создания объектов DTO, когда необходимо использовать словарь с определенным набором ключей.
Для создания объекта необходимо импортировать тип данных TypedDict из модуля typing. Давайте создадим 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 использует аннотации типов для определения полей, а затем генерирует все методы, необходимые для создания и использования объектов этого класса.
Для создания DTO с помощью dataclass нужно добавить декоратор dataclass и определить поля с аннотациями типов. Например, мы можем создать DTO для модели пользователя с помощью dataclass следующим образом:
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("last_name length must be more then 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: last_name length must be more then 2
Чтобы создать неизменяемый объект нужно в декаратор передавать аргумент frozen=True. Есть метод asdict для выгрузки в словарь. Дополнительно можно реализовать методы валидации. Можно использовать значения по умолчанию. В целом более компактные чем просто классы и более функциональные чем ранее рассмотренные варианты.
Подробнее тут.
Attr
Еще один способ создания DTO это модуль Attr. Работает точно так же как и dataclass, кроме того, является предком 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))
Здесь мы с помощью декоратора определили класс для описания DTO c атрибутами first_name и last_name, при этом сразу определили значения по умолчанию и валидацию.
>>> 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'>...
Таким образом, модуль attr предоставляет более мощные и гибкие инструменты для определения классов DTO, такие как валидация, значения по умолчанию, преобразования. Объект DTO можно также сделать неизменяемым с помощью аттрибута декоратора frozen=True. Так же может быть инициализирован через декоратор define.
Подробнее тут.
Pydantic
Библиотека Pydantic представляет собой инструмент для определения данных и конвертации данных в Python, который использует аннотации типов для определения схемы данных и преобразует данные из JSON в объекты Python. Pydantic используется для удобной работы с данными веб-запросов, конфигурационных файлов, баз данных и других мест, где необходимо проверять и преобразовывать данные. Может быть установлен с помощью команды 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="Age must be a positive integer")
@field_validator("age")
def validate_age(cls, value):
if value < 18:
raise ValueError("Age must be at least 18")
return value
Здесь мы определили модель UserDTO c базовой валидацией на длину строки и максимум возраста. Так же определили что данные для атрибута 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 c помощью встроенных методов. Имеет более читаемые подсказки во время выполнения.
Подробнее тут.
Заключение
В данной статье я пробежался по вариантам реализации DTO в Python от простого к более сложным. Какой в итоге выбрать для реализации на своём проекте зависит от многих факторов. Какая версия Python на проекте и есть ли возможность установки новых зависимостей. Планируется ли использовать валидацию или конвертацию или достаточно простой аннотации типов.
Надеюсь эта статья поможет тем кто ищет подходящие способы реализации DTO в Python.