Основной целью 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.
