- PVSM.RU - https://www.pvsm.ru -

Простота естественного разрастания кода не лишена и обратной стороны — такой код становится трудно обслуживать. Количественное размножение словарей в качестве основных структур данных чётко сигнализирует о наличии технического долга. К счастью, сегодня Python предоставляет для простых словарей много адекватных альтернатив.
dataclasses. TypedDict.Функции, которые получают словари, очень трудно расширять и изменять. Как правило, для изменения функции, получающей словарь, нужно вручную отследить её вызовы вплоть до источников, где создавался словарь. Зачастую существует не один путь вызова, и если программа разрастается без чёткого плана, в структурах словарей наверняка возникнут расхождения.
Изменение значений словарей для соответствия конкретному рабочему потоку может быть весьма заманчивым. И программисты нередко этим грешат. Изменения на конкретных местах могут происходить под разными именами: предварительная обработка, заполнение, расширение, обработка данных и так далее. Но результат будет один и тот же. Подобные действия нарушают структуру данных и делают их зависимыми от рабочего потока приложения.
Причём словари позволяют изменять не только свои данные, но и структуру объектов. Вы можете добавлять или удалять поля, а также изменять их типы. Вот только всё это является худшим, что можно сделать с данными.
Как правило, словари в коде создаются путём десереализации JSON, полученного, к примеру, из ответа стороннего API.
Словарь, возвращённый из API:
>>> requests.get("https://api.github.com/repos/imankulov/empty").json()
{'id': 297081773,
'node_id': 'MDEwOlJlcG9zaXRvcnkyOTcwODE3NzM=',
'name': 'empty',
'full_name': 'imankulov/empty',
'private': False,
...
}
Выработайте привычку воспринимать словари как формат передачи информации и сразу преобразовывайте их в структуры данных, обеспечивающие семантическую ясность.

Используйте сериализацию/десериализацию для преобразования данных между форматом сетевой передачи и внутренним представлением
Реализовать это легко.
В предметно-ориентированном дизайне (Domain-Driven Design, DDD) этот паттерн известен как предохранительный уровень (anti-corruption layer). Помимо семантической ясности, предметная модель обеспечивает естественный уровень, отделяющий внешнюю архитектуру от бизнес-логики приложения.
Ниже я приведу две реализации функции, извлекающей с GitHub информацию о репозитории:
Возвращение словаря:
import requests
def get_repo(repo_name: str):
"""Return repository info by its name."""
return requests.get(f"https://api.github.com/repos/{repo_name}").json()
Вывод такой функции будет непонятным и излишне громоздким, так как его формат определяется вне вашего кода.
>>> get_repo("imankulov/empty")
{'id': 297081773,
'node_id': 'MDEwOlJlcG9zaXRvcnkyOTcwODE3NzM=',
'name': 'empty',
'full_name': 'imankulov/empty',
'private': False,
# Множество строк ненужных атрибутов, URL и прочего.
# ...
}
Модель предметной области:
class GitHubRepo:
"""GitHub repository."""
def __init__(self, owner: str, name: str, description: str):
self.owner = owner
self.name = name
self.description = description
def full_name(self) -> str:
"""Get the repository full name."""
return f"{self.owner}/{self.name}"
def get_repo(repo_name: str) -> GitHubRepo:
"""Return repository info by its name."""
data = requests.get(f"https://api.github.com/repos/{repo_name}").json()
return GitHubRepo(data["owner"]["login"], data["name"], data["description"])
>>> get_repo("imankulov/empty")
<GitHubRepo at 0x103023520>
Несмотря на то, что второй пример содержит больше кода, такое решение окажется лучше предыдущего, если речь идёт о поддержке и расширении кодовой базы.
В чём же объективные отличия.
full_name(), реализующий его бизнес-логику. В отличие от словарей модели данных позволяют размещать код и данные рядом.get_repo(). Объекту GitHubRepo не нужно ничего знать о внешнем API и создании объектов. Благодаря этому, вы можете изменять десериализатор независимо от модели или добавлять новые способы создания объектов: из фикстур pytest, GraphQL API, локального кэша и так далее.☝️ Игнорируйте поля, полученные от API, если они вам не нужны, оставляя только те, которые используете.
Во многих случаях вам следует игнорировать большинство полей, получаемых от API, и добавлять только те, которые использует приложение. Дублирование полей не только является пустой тратой времени, но и лишает структуру гибкости, усложняя внесение изменений в бизнес-логику или добавление поддержки в новые версии API. С позиции тестирования, чем меньше полей, тем меньше проблем с инстанцированием объектов.
Для обёртывания словарей нужно множество классов. В этом плане вы можете упростить себе работу с помощью библиотеки, которая будет создавать для вас «более качественные классы».
Начиная с v 3.7, в Python появились Data Classes [11]. Модуль dataclasses стандартной библиотеки предоставляет декоратор и функции для автоматического добавления в классы специально сгенерированных методов вроде __init__() и __repr__(). В итоге шаблонного кода писать приходится меньше.
Я использую классы данных для небольших проектов или скриптов, когда не хочу вносить лишние зависимости. Вот как выглядит модель GitHubRepo с классами данных:
from dataclasses import dataclass
@dataclass(frozen=True)
class GitHubRepo:
"""GitHub repository."""
owner: str
name: str
description: str
def full_name(self) -> str:
"""Get the repository full name."""
return f"{self.owner}/{self.name}"
Когда я создаю классы данных, они почти всегда определяются как фиксированные (frozen). Вместо изменения объекта я создаю новый экземпляр при помощи dataclasses.replace(). Используя атрибуты только для чтения, вы облегчаете жизнь разработчику, который будет читать или обслуживать ваш код.
Недавно я начал использовать для определения моделей библиотеку Pydantic [12], отвечающую за проверку сторонних данных. Если сравнивать её с классами данных, то она намного функциональней. Мне особенно нравятся её сериализаторы и десериализаторы, автоматическое преобразование типов и кастомные валидаторы.
Сериализаторы упрощают сохранение записей во внешнее хранилище, например, для кэширования. Преобразование типов особенно помогает в случае превращения сложных иерархических документов JSON в иерархию объектов. Валидаторы же пригождаются в остальных задачах.
В случае Pydantic та же модель может выглядеть так:
from pydantic import BaseModel
class GitHubRepo(BaseModel):
"""GitHub repository."""
owner: str
name: str
description: str
class Config:
frozen = True
def full_name(self) -> str:
"""Get the repository full name."""
return f"{self.owner}/{self.name}"
Онлайн-сервис jsontopydantic.com [13] экономит моё время, создавая модели Pydantic из данных, получаемых от сторонних API. Я копирую в этот сервис примеры ответов из документации, и он возвращает модели Pydantic.

jsontopydantic.com преобразует ответ Todoist API в модель Pydantic
Примеры моего использования Pydantic можно найти в статье «Time Series Caching with Python and Redis [14]».
TypedDictВ Python 3.8 появились так называемые TypedDicts [15]. В среде выполнения они действуют как обычные словари, но предоставляют дополнительную информацию о своей структуре для разработчиков, валидаторов типов и IDE.
Если вы встретите насыщенный словарями легаси-код и не будете понимать, как его полноценно отрефакторить, то хотя бы аннотируйте все словари как типизированные.
from typing import TypedDict
class GitHubRepo(TypedDict):
"""GitHub repository."""
owner: str
name: str
description: str
repo: GitHubRepo = {
"owner": "imankulov",
"name": "empty",
"description": "An empty repository",
}
Ниже я привёл два скриншота из PyCharm, чтобы показать, каким образом добавление информации типа может упростить процесс разработки в IDE и защитить от ошибок.

PyCharm знает тип значения и предоставляет варианты автозавершения

PyCharm знает о недостающем ключе и выдаёт предупреждение
Оправданным случаем применения словарей является хранилище пар ключ-значение, где все значения имеют один тип, а ключи используются для их поиска.
Словарь, используемый как мэппинг:
colors = {
"red": "#FF0000",
"pink": "#FFC0CB",
"purple": "#800080",
}
При инстанцировании или передаче такого словаря в функцию подумайте о том, чтобы скрыть детали реализации, аннотировав тип переменной как Mapping или MutableMapping. С одной стороны, это может показаться перебором, ведь словарь является дефолтной и пока что наиболее типичной реализацией MutableMapping. С другой же стороны, аннотируя переменную как мэппинг, вы можете указывать типы для ключей и значений. Кроме того, в случае типа Mapping вы ясно указываете, что объект предполагает изменения.
Вот пример, где я определил мэппинг цветов и аннотировал функцию. Заметьте, что функция использует операцию, разрешённую для словарей, но недопустимую для экземпляров Mapping:
# файл: colors.py
from typing import Mapping
colors: Mapping[str, str] = {
"red": "#FF0000",
"pink": "#FFC0CB",
"purple": "#800080",
}
def add_yellow(colors: Mapping[str, str]):
colors["yellow"] = "#FFFF00"
if __name__ == "__main__":
add_yellow(colors)
print(colors)
Несмотря на неверные типы, в среде выполнения проблем не обнаруживается.
$ python colors.py
{'red': '#FF0000', 'pink': '#FFC0CB', 'purple': '#800080', 'yellow': '#FFFF00'}
Для проверки валидности я использую mypy, которая в данном случае выдаёт ошибку.
$ mypy colors.py
colors.py:11: error: Unsupported target for indexed assignment ("Mapping[str, str]")
Found 1 error in 1 file (checked 1 source file)
Следите за словарями. Не позволяйте им захватить ваше приложение. Как и в случае любого элемента технического долга, чем дольше вы откладываете внедрение подходящих структур данных, тем сложнее на них в итоге перейти.
Автор: Bright_Translate
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/413888
Ссылки в тексте:
[1] Что не так со словарями?: #1
[2] Словари непрозрачны.: #2
[3] Словари изменяемы.: #3
[4] Рассматривайте словари как формат передачи данных.: #4
[5] Упрощайте создание моделей.: #5
[6] Создавайте модели с помощью: #6
[7] Либо создавайте модели с помощью Pydantic.: #7
[8] В легаси-коде аннотируйте словари как : #8
[9] В хранилищах пар ключ-значение аннотируйте словари как мэппинги.: #9
[10] Возьмите словари под контроль.: #10
[11] Data Classes: https://docs.python.org/3/library/dataclasses.html
[12] Pydantic: https://pydantic-docs.helpmanual.io/
[13] jsontopydantic.com: https://jsontopydantic.com/
[14] Time Series Caching with Python and Redis: https://roman.pt/posts/time-series-caching/
[15] TypedDicts: https://www.python.org/dev/peps/pep-0589/
[16] Источник: https://habr.com/ru/companies/ruvds/articles/890402/?utm_source=habrahabr&utm_medium=rss&utm_campaign=890402
Нажмите здесь для печати.