default_factory в dataclass: отдельный список для каждого объекта

Две корзины должны хранить разные товары. При обычном классе список в аргументе по умолчанию легко становится общим. dataclass обнаруживает многие изменяемые значения по умолчанию, но правильный выбор фабрики всё равно остаётся за автором.

Фабрика вызывается для нового экземпляра

field(default_factory=list) означает: когда поле не передано, вызвать list и взять новый результат. Это отличается от одного списка, созданного при определении класса. Передавать нужно саму функцию list, без круглых скобок. Тогда каждый конструктор получает новую коллекцию.

Фабрика может быть пользовательской функцией, например создающей начальные товары. Она тоже должна возвращать независимый объект, если этого требует модель. lambda: shared_list технически является фабрикой, но каждый вызов вернёт одну и ту же ссылку. Название default_factory не гарантирует копирование.

Проверять независимость через поведение

В примере две корзины создаются без аргумента items. В первую добавляется товар, вторая остаётся пустой. Дополнительный is not закрепляет различие объектов, но главная проверка показывает пользовательский эффект. Затем отдельно проверяем фабрику начальных тегов: изменение одного профиля не меняет другой.

Схема хранения остаётся простой: элементы — строки, поэтому дополнительное копирование вложенных объектов не нужно. Если фабрика создаёт список словарей, проверьте и независимость словарей. Нового внешнего списка недостаточно, если внутри сохранились общие изменяемые значения.

from dataclasses import dataclass, field

@dataclass
class Cart:
    items: list[str] = field(default_factory=list)

def initial_tags() -> list[str]:
    return ["new"]

@dataclass
class Profile:
    tags: list[str] = field(default_factory=initial_tags)

first, second = Cart(), Cart()
first.items.append("book")
assert first.items == ["book"]
assert second.items == []
assert first.items is not second.items
a, b = Profile(), Profile()
a.tags.append("active")
assert b.tags == ["new"]
print(first, second, b)

Явно переданный список имеет другого владельца

Если вызвать Cart(existing_list), фабрика не используется. Поле получит переданный объект, и его изменения будут видны с обеих сторон. Решите, допустим ли такой обмен. Для снимка можно создать отдельный конструктор с копированием или хранить кортеж. Для живой общей коллекции ссылку можно оставить, но договорённость должна быть явной.

Различайте два вопроса: независимы ли значения по умолчанию и независим ли любой вход. default_factory решает первый. Тесты стоит строить отдельно, чтобы корректная фабрика не создала ложную уверенность в полном владении входными данными.

Выбрать подходящий объект данных

Откройте практику выбора Dataclass, NamedTuple и класса. Создайте структуру и проверьте её поля на двух экземплярах. Посмотрите программу, чтобы дальше работать с инвариантами и тестированием объектов.

Продолжить на практике

  • Python для продвинутых: ООП, типизация и тестирование ↗

    Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.