frozen dataclass: почему вложенный список всё ещё меняется
Настройки пометили frozen=True и передали в расчёт. Позже другой участок добавил элемент в список правил, и уже созданные настройки изменились. Заморозка dataclass запрещает присваивание полю, но не замораживает объект внутри поля.
У снимка должна быть собственная граница
Пусть запуск хранит коды разрешённых складов. Если в поле лежит список вызывающего кода, обе стороны видят один объект. frozen перехватывает попытку settings.warehouses = другое_значение. Вызов settings.warehouses.append меняет сам список и не выполняет присваивания полю, поэтому запрет не срабатывает.
Если нужен снимок на момент запуска, преобразуйте вход в неизменяемое представление. Для последовательности строк подойдёт tuple. В примере преобразование сделано в отдельном фабричном методе: наружу ясно видно, где принимается изменяемый вход и создаётся независимое значение. Поля имеют типы, соответствующие фактическому хранению.
Не копировать больше, чем требует договорённость
Кортеж строк достаточен, потому что строки тоже неизменяемы. Кортеж словарей оставит словари изменяемыми. Для такой структуры выберите dataclass для каждого элемента с неизменяемыми полями либо явно создайте вложенные копии и определите, кто вправе их менять. Слово frozen само по себе не отвечает на этот вопрос.
Проверим два действия: изменение исходного списка после создания настроек и попытку переприсвоить поле. Первое не влияет на снимок, второе вызывает FrozenInstanceError. Это разные гарантии, и обе нужны сценарию воспроизводимого запуска.
from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True)
class Settings:
warehouses: tuple[str, ...]
@classmethod
def from_list(cls, values: list[str]):
return cls(tuple(values))
source = ["north", "south"]
settings = Settings.from_list(source)
source.append("east")
assert settings.warehouses == ("north", "south")
try:
settings.warehouses = ("east",)
except FrozenInstanceError:
pass
else:
raise AssertionError("field assignment must be forbidden")
assert source == ["north", "south", "east"]
print(settings)Как применять это к ключам и истории запусков
Неизменяемые значения удобны для ключей кеша и записи истории. Однако автоматически сгенерированный hash сможет работать только тогда, когда хешируемы сами поля. Список внутри frozen dataclass останется проблемой и для хеширования. Выбор tuple одновременно выражает смысл снимка и делает структуру подходящей для обычного хеширования строковых значений.
Проверку входных значений фабрика здесь не выполняет: контракт уже принимает list[str]. Если данные пришли из JSON, сначала проверьте их структуру и значения на границе загрузки. Аннотации Python сами не выполняют runtime-валидацию. После проверки удобно перейти к узкому неизменяемому объекту, которым пользуется остальная программа.
Выбрать структуру по поведению
Откройте публичный урок о Dataclass, NamedTuple и обычном классе. Сравните структуры и закрепите создание объекта данных. Посмотрите программу, чтобы затем связать объект с инвариантами и тестами.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
