惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
Docker
Apple Machine Learning Research
Apple Machine Learning Research
宝玉的分享
宝玉的分享
博客园 - 叶小钗
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - Franky
爱范儿
爱范儿
罗磊的独立博客
IT之家
IT之家
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
Netflix TechBlog - Medium
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
U
Unit 42
Engineering at Meta
Engineering at Meta
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
T
Tailwind CSS Blog
H
Help Net Security
博客园_首页
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
人人都是产品经理
人人都是产品经理

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
3 ошибки при работе с dataclasses в Python
badcasedaily · 2026-05-19 · via Все публикации подряд на Хабре

Уровень сложностиПростой

Время на прочтение5 мин

Охват и читатели6

Обзор

Dataclasses появились в Python 3.7 и быстро стали стандартом: меньше бойлерплейта, чем у обычных классов, проще, чем attrs, и не требуют зависимостей. Выглядят настолько просто, что кажется, что ломаться там нечему. Но у них есть три ловушки, которые не видны при написании.

Ошибка 1: mutable default в полях

Вот этот код:

from dataclasses import dataclass

@dataclass
class UserConfig:
    name: str
    permissions: list[str] = []

...не компилируется. Python выдаёт ValueError: mutable default value и отказывается создавать класс. Это хорошо: ошибка ловится сразу, разработчик идёт в документацию и узнаёт про field(default_factory=...).

Проблема в другом. Часто разработчик «исправляет» ошибку вот так:

@dataclass
class UserConfig:
    name: str
    permissions: list[str] = None

    def __post_init__(self):
        if self.permissions is None:
            self.permissions = []

Формально работает, но permissions теперь Optional[list[str]], хотя по смыслу это всегда список. Mypy будет ругаться (или молчать с type: ignore), и каждое место использования будет либо проверять на None, либо игнорировать тип. Через полгода кто-то передаст None явно, думая, что это допустимо, и получит баг.

Правильный способ:

from dataclasses import dataclass, field

@dataclass
class UserConfig:
    name: str
    permissions: list[str] = field(default_factory=list)

default_factory вызывается при каждом создании экземпляра. Каждый UserConfig получает свой пустой список. Тип всегда list[str], никакого Optional, mypy доволен.

Для более сложных значений по умолчанию — lambda:

@dataclass
class PipelineConfig:
    name: str
    steps: list[str] = field(default_factory=lambda: ["validate", "transform", "load"])
    metadata: dict[str, str] = field(default_factory=dict)

Каждый экземпляр получает свою копию steps и свой пустой dict. Мутация одного экземпляра не затрагивает другие.

Казалось бы, мелочь. Но часто бывает так, что один экземпляр UserConfig мутировал shared-список permissions, и все остальные экземпляры (созданные до исправления, когда default был списком, а не factory) внезапно получали чужие permissions. Р

Ошибка 2: frozen=True не делает объект неизменяемым

Многие разработчики думают, что frozen=True делает dataclass иммутабельным. Это не так.

@dataclass(frozen=True)
class Report:
    title: str
    tags: list[str] = field(default_factory=list)

report = Report(title="Q1", tags=["finance"])

frozen=True запрещает присваивание в поля. report.title = "Q2" вызовет FrozenInstanceError. Пока всё логично. Но вот это работает без ошибок:

report.tags.append("urgent")
print(report.tags)  # ['finance', 'urgent'] — объект изменился!

frozen=True запрещает setattr (переназначение поля), но не запрещает мутацию содержимого поля. report.tags = new_list упадёт. report.tags.append(item) пройдёт. Python не может запретить мутацию произвольного объекта, потому что не знает, какие его методы меняют состояние, а какие нет.

Frozen-датаклассы часто используют как ключи словарей и элементы множеств, потому что frozen=True генерирует hash. И вот что получается:

cache = {}
key = Report(title="Q1", tags=["finance"])
cache[key] = "some result"

key.tags.append("urgent")  # мутируем содержимое
print(cache[key])           # KeyError! Хеш изменился, ключ потерян.

Хеш объекта вычисляется при первом обращении и зависит от содержимого полей. Когда вы мутируете список внутри frozen-объекта, хеш меняется, и словарь не может найти ключ, потому что ищет по новому хешу в бакете, куда объект попал по старому.

Если нужна настоящая иммутабельность, используйте иммутабельные типы для всех полей:

@dataclass(frozen=True)
class Report:
    title: str
    tags: tuple[str, ...]  # tuple вместо list

report = Report(title="Q1", tags=("finance",))
# report.tags.append("urgent")  # AttributeError: tuple has no append

Tuple, frozenset, str — иммутабельные. Если все поля frozen-датакласса используют такие типы, объект действительно неизменяемый. Если хотя бы одно поле мутабельное, frozen защищает только от переназначения, но не от изменения содержимого.

Правило простое: frozen-датакласс, который используется как ключ или элемент множества, должен содержать только хешируемые и иммутабельные поля. Если нужен список, оберните в tuple. Если нужен set, используйте frozenset. Если нужен dict... ну, тут сложнее, но types.MappingProxyType или просто отказ от dict в frozen-классе.

Ошибка 3: наследование ломает сравнение

Это самая неприятная из трёх ошибок, потому что результат зависит от порядка операндов.

@dataclass
class Animal:
    name: str
    weight: float

@dataclass
class Dog(Animal):
    breed: str

animal = Animal(name="Rex", weight=30.0)
dog = Dog(name="Rex", weight=30.0, breed="Labrador")

print(animal == dog)  # True
print(dog == animal)  # False

animal == dog вызывает Animal.__eq__, который сравнивает только поля Animal: name и weight. Они совпадают. Про breed Animal ничего не знает, поэтому не проверяет. Результат: True.

dog == animal вызывает Dog.__eq__, который сравнивает все три поля: name, weight, breed. У animal нет breed, Dog.eq возвращает NotImplemented, Python переворачивает и пробует Animal.eq, который опять сравнивает только name и weight. Результат: False.

a == b и b == a дают разные результаты. Это нарушает контракт равенства (симметричность), и последствия проявляются в самых неожиданных местах:

animals = {animal, dog}
print(len(animals))  # 1 или 2? Зависит от порядка вставки.

Set хеширует объекты и проверяет равенство при коллизии. Если animal добавлен первым, а dog при вставке сравнивается с ним через animal == dog (True), set решит, что это дубликат, и не добавит. Если dog первый, а animal сравнивается через dog == animal (False), оба останутся.

Исправить можно тремя способами. Первый — проверять тип в eq:

@dataclass
class Animal:
    name: str
    weight: float

    def __eq__(self, other):
        if type(other) is not type(self):
            return NotImplemented
        return (self.name, self.weight) == (other.name, other.weight)

Теперь Animal("Rex", 30) == Dog("Rex", 30, "Lab") вернёт NotImplemented с обеих сторон, и Python вернёт False. Симметрично.

Второй — использовать eq=False на родителе и генерировать eq только на дочернем классе. Третий — не наследовать датаклассы друг от друга и использовать композицию:

@dataclass
class AnimalInfo:
    name: str
    weight: float

@dataclass
class Dog:
    info: AnimalInfo
    breed: str

Третий вариант самый безопасный, потому что не создаёт проблем с eq, hash и порядком полей. Наследование датаклассов работает, но требует внимания к деталям, которые очень легко пропустить.

Ловушка с порядком полей при наследовании

Ещё одна проблема, которая связана с наследованием, но не с равенством:

@dataclass
class Base:
    name: str
    value: int = 0  # поле с дефолтом

@dataclass
class Child(Base):
    label: str  # поле без дефолта — TypeError!

Python объединяет поля в порядке MRO: name, value (с дефолтом), label (без дефолта). Поля без дефолта после полей с дефолтом запрещены (как в обычных функциях). Решение для Python 3.10+:

@dataclass
class Child(Base):
    label: str = field(kw_only=True)

child = Child(name="test", label="important")  # value=0 по умолчанию

kw_only=True делает поле keyword-only аргументом, и оно не участвует в позиционном порядке.

Итого

Три пункта, которые стоит проверить перед тем, как dataclass попадёт в прод. Все мутабельные дефолты через field(default_factory=...), никаких = [] и = {} и тем более = None с __post_init__. Если frozen=True, все поля иммутабельных типов, иначе frozen защищает только от переназначения. Если наследуете датаклассы, проверьте eq на симметричность и подумайте, не лучше ли композиция.

Если вы видели другие проблемы с dataclasses, пишите в комментариях. Спасибо, что дочитали.


Такие ошибки редко ломают проект сразу, но хорошо показывают, насколько глубоко вы понимаете Python за пределами синтаксиса.

Если вы уже пишете на Python и хотите системно подтянуть уровень — архитектуру, асинхронность, производительность, типизацию и внутреннее устройство языка — можно начать с вступительного теста на курс «Python-разработчик». Он поможет понять, насколько текущего опыта достаточно для обучения на продвинутом уровне.

Практические разборы от экспертов в разработке ищите в календаре бесплатных демо-уроков.