Dataclasses и attrs: когда и почему
В Python 3.7 были представлены dataclasses (PEP557). Dataclasses могут быть удобным способом создания классов, основная цель которых состоит в том, чтобы содержать значения.
Дизайн dataclasses основан на существующей библиотеке attr.s. На самом деле Гинек Шлавак (Hynek Schlawack) является автором attrs и он же помог с написанием PEP557.
По сути, dataclasses — это уменьшенная версия attrs. Хорошо это или плохо, зависит от вашего конкретного варианта использования.
Я думаю, что добавление dataclasses в стандартную библиотеку делает attrs еще более актуальными. Я рассматриваю это так, что одно является подмножеством другого, и хорошо что существуют оба варианта. И вам, вероятно, придется так же использовать оба в своих проектах, в соответствии с уровнем формальности, который вы хотите установить в конкретном фрагменте кода.
В этой статье я покажу, как я использую dataclasses и attrs, почему я думаю, что вы должны использовать оба, и почему я считаю, что attrs все еще очень актуальны.
Что они делают
И dataclasses стандартной библиотеки, и библиотека attrs обеспечивают способ определения того, что я называю «структурированные типы данных» (так же как namedtuple, dict и typeddict)
Все они являются вариациями одного и того же понятия: класс, представляющий тип данных, содержащий несколько значений, каждое из которых адресуется каким-либо ключом.
Они также делают еще несколько полезных вещей: они обеспечивают упорядочение (ordering), сериализацию (serialization) и хорошее представление строк. Но по большей части наиболее полезной целью является добавление определенной степени формализации к группе значений, которые необходимо передать.
Пример
Я думаю, что пример лучше иллюстрирует, для чего я использую dataclasses и attrs. Предположим, вы хотите визуализировать шаблон, содержащий таблицу. И вам нужно убедиться, что таблица имеет заголовок, описание и строки:
def render_document(title: str, caption: str, data: List[Dict[str, Any]]): return template.render({ "title" : title, "caption": caption, "data": data, })
Теперь предположим, что вы хотите визуализировать документ, который состоит из title, description, status («draft», «in review», «approved») и списка таблиц. Как бы вы передали таблицы в render_document?
Вы можете выбрать для представления каждой таблицы словарь dict:
{ "title": "My Table", "caption": "2019 Earnings", "data": [ {"Period": "QT1", "Europe": 500, "USA": 467}, {"Period": "QT2", "Europe": 345, "USA": 765}, ] }
Но как бы вы описали аннотацию типа для tables, чтобы он был правильным, явным и простым для понимания?
def render_document(title: str, description: str, status: str, tables: List[Dict[str, Any]]): return template.render({ "title": title, "description": description, "status": status, "tables": tables, })
Но такое описание только позволяет нам описать первый уровень tables. Это не определяет того, что Table должна иметь title или caption. Вместо этого вы можете использовать dataclass:
@dataclass class Table: title: str data: List[Dict[str, Any]] caption: str = "" def render_document(title: str, description: str, tables: List[Table]): return template.render({ "title": title, "description": description, "tables": tables, })
Таким образом, у нас есть подсказки типов, помогая нашей IDE помогать нам.
Но мы можем пойти еще дальше, также обеспечив проверку типа во время выполнения. Это где dataclasses останавливаются, и на помощь нам приходит attrs:
@attr.s class Table(object): title: str = attr.ib(validator=attr.validators.instance_of(str)) # don't you pass no bytes! data: List[Dict[str, Any]] = attr.ib(validator=...) description: str = attr.ib(validator=attr.validators.instance_of(str), default="") def render_document(title: str, description: str, tables: List[Table]): return template.render({ "title": title, "description": description, "tables": tables, })
Теперь предположим, что нам также нужно визуализировать «Report», который представляет собой набор «Document». Вы, вероятно, можете понять из кода что тут происходит:
@dataclass class Table: title: str data: List[Dict[str, Any]] caption: str = "" @attr.s class Document(object): status: str = attr.ib(validators=attr.validators.in_( ["draft", "in review", "approved"] )) tables: List[Table] = attr.ib(default=[]) def render_report(self, title: str, documents: List[Document]): return template.render({ "title": title, "documents": documents, })
Обратите внимание, как я проверяю, что Document.status содержит одно из допустимых значений. Это особенно удобно, когда вы строите абстракции поверх моделей Django с полем, в котором используются choices. Dataclasses не могут этого сделать.
Вот несколько паттернов выбора нужно библиотеки, которыми я постоянно пользуюсь:
- Вначале создаю функцию, которая принимает некоторые аргументы
- Группирую некоторые аргументы в tuple
- Если я хочу использовать только имена полей выбираю namedtuple.
- Если я хочу использовать еще и определение типов выбираю dataclass.
- Если я хочу использовать еще и валидацию типов, выбираю attrs.
Еще пример ситуации, которая случается у меня довольно часто:
- Пишу функцию, которая принимает некоторые аргументы
- Добавляю определение типов typing, чтобы моя IDE могла помочь мне
- Проводу рефакторинг для использования dataclasses, там где это возможно
- Далее я проверяю может ли аргумент функции принимать незапланированное значение, если да, я спрашиваю себя: как мне убедиться, что другие разработчики передают правильный тип и/или значения? И в случае необходимости использую attrs
Иногда я останавливаюсь в dataclasses, но часто я выбираю attrs.
Сравнение
Две библиотеки выглядят очень похожими. Чтобы получить более четкое представление о том, как они сравниваются, я составил таблицу наиболее часто используемых функций:
свойство | dataclasses | attrs |
---|---|---|
frozen | ✓ | ✓ |
defaults | ✓ | ✓ |
totuple | ✓ | ✓ |
todict | ✓ | ✓ |
validators | ✗ | ✓ |
converters | ✗ | ✓ |
slotted classes | ✗ | ✓ |
Как видите, много общего. Но дополнительные функции в attrs обеспечивают функциональность, которая мне нужна чаще всего.
Когда использовать dataclasses
Dataclasses — это просто форма данных. Выберите dataclasses, если:
- Вам не важны значения в полях, только их тип
Когда использовать attrs
Выберите attrs, если:
- Вы хотите проверить значения. Распространенным случаем будет эквивалент ChoiceField.
- Вы хотите normalize или sanitize входные данные
- Вы хотите больше формализации, чем могут предложить одни только dataclasses
- Вы обеспокоены потреблением памяти. attrs может создавать сегментированные классы (slotted classes), которые оптимизируются с помощью CPython.
Я часто использую dataclasses, а потом переключаюсь на attr.s, потому что меняются требования, или я обнаружил, что мне нужно защитить данные. Я думаю, что это нормальный аспект разработки программного обеспечения и то, что я называю «непрерывный рефакторинг».
Почему мне нравятся dataclasses
Я рад, что dataclasses были добавлены в стандартную библиотеку, и я думаю, что это выгодное дополнение.
Это так же будет поощрять более структурированный стиль программирования с самого начала. Но я думаю, что наиболее убедительный аргумент за — это практическая ситуация. В некоторых высокорискованных корпоративных средах (например, в финансовых учреждениях) требуется проверка каждого фрагмента кода (по уважительной причине: были же случаи вредоносного кода в библиотеках). Но возможно, что добавление attrs не так просто, как добавить строку в файл requirements.txt, иногда это требует ожидания одобрения со стороны руководства. Но разработчики могут сразу использовать dataclasses, и их код сразу выиграет от использования более формализованных типов данных.
Почему я люблю attrs
Большинство людей не работают в таких строго контролируемых условиях.
И конечно, иногда вам не нужны все функции от attrs, но это не мешает их иметь.
Чаще всего я все равно нуждаюсь в них, поскольку я формализую все больше и больше API моего кода.
Заключение
Я думаю, что dataclasses охватывают только подмножество того, что может предложить attrs. По общему признанию, это большая подгруппа функционала. Но функции, которые в него не входят, так же достаточно важны и достаточно часто необходимы, чтобы сделать attrs не только актуальным и полезным, но и необходимым.
На мой взгляд, использование обоих библиотек позволяет разработчикам постепенно проводить рефакторинг своего кода, перемещая уровень формализации от свободно определенных аргументов до формально описанных структур данных, когда требования приложения со временем стабилизируются.
Одним из приятных эффектов наличия dataclasses является то, что теперь разработчики получают больше стимулов для рефакторинга своего кода в направлении большей формализации. В какой-то момент dataclasses будет недостаточно, и тогда разработчики будут рефакторировать использование attrs. Таким образом, dataclasses фактически действуют как приглашение к использованию attrs. Я не удивлюсь, если attrs станет более популярным благодаря dataclasses.
Оригинальная статья: Flavio Curella Dataclasses and attrs: when and why