Перейти к содержанию

232 уроков, 16 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 16 из 20 Средний 30 мин 130 XP

@dataclass: классы без бойлерплейта

Декоратор @dataclass пишет за вас __init__, __repr__ и __eq__ по описанным полям класса. Разберём значения по умолчанию, field(default_factory) для списков, frozen=True для неизменяемых объектов и __post_init__ для производных полей.

Редакция Питоники

Класс-данные в чистом Python начинается с рутины: в __init__ по строке на каждое поле, затем __repr__, чтобы объект читался в отладке, затем __eq__, чтобы два одинаковых товара считались равными. Три метода на пять полей — экран бойлерплейта. Декоратор @dataclass берёт эту работу на себя: вы описываете только поля, он генерирует методы. Слева — что приходилось писать руками, справа — что достаточно теперь:

слева руками, справа dataclass — результат одинаковый
class ProductManual:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"Product(name={self.name!r}, price={self.price!r})"

    def __eq__(self, other):
        if not isinstance(other, ProductManual):
            return NotImplemented
        return (self.name, self.price) == (other.name, other.price)

from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: int

a = Product("кружка", 350)
b = Product("кружка", 350)
print(a)
print(a == b)
Вывод
Product(name='кружка', price=350)
True
МетодЧто генерирует @dataclass
__init__Поля становятся параметрами конструктора и записываются в self по порядку
__repr__Печатает имя класса и все поля: Product(name='кружка', price=350)
__eq__Сравнивает два объекта попарно по полям: одинаковые данные — равные объекты

Обратите внимание на аннотации name: str и price: int: для @dataclass они обязательны — именно по их наличию декоратор отличает поля класса от обычных атрибутов. Значения по умолчанию описываются прямо в списке полей, как у обычных аргументов функции: после полей без значений могут идти только поля со значениями.

значения по умолчанию
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: int = 100
    in_stock: bool = True

print(Product("степлер"))
print(Product("точилка", 80))
Вывод
Product(name='степлер', price=100, in_stock=True)
Product(name='точилка', price=80, in_stock=True)
field(default_factory=list): у каждого заказа свой список
from dataclasses import dataclass, field

@dataclass
class Order:
    number: int
    items: list = field(default_factory=list)
    tags: list = field(default_factory=list)

    def add(self, item):
        self.items.append(item)

o1 = Order(1)
o2 = Order(2)
o1.add("ручка")
o1.tags.append("срочно")

print(o1)
print(o2)
Вывод
Order(number=1, items=['ручка'], tags=['срочно'])
Order(number=2, items=[], tags=[])

frozen=True и __post_init__

frozen=True: неизменяемый и хешируемый
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(2, 3)

print(p in {Point(2, 3), Point(4, 1)})
print(len({Point(2, 3), Point(2, 3)}))

try:
    p.x = 10
except Exception as e:
    print(type(e).__name__)
Вывод
True
1
FrozenInstanceError
Обычный dataclass не хешируем: __hash__ == None при eq=True. frozen=True поднимает это ограничение — точки можно класть в множество и ключи словаря.
__post_init__: производные поля после конструктора
from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: int
    height: int
    area: int = field(init=False)

    def __post_init__(self):
        self.area = self.width * self.height

r1 = Rectangle(3, 4)
r2 = Rectangle(5, 5)
print(r1)
print(r2.area)
Вывод
Rectangle(width=3, height=4, area=12)
25
field(init=False) исключает area из параметров __init__ — его вычисляет __post_init__, который dataclass вызывает сразу после сгенерированного конструктора.
Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from dataclasses import dataclass

@dataclass
class Cat:
    name: str
    age: int = 1

a = Cat("Барсик")
b = Cat("Барсик", 1)
print(a == b)
print(a)
from dataclasses import dataclass, field

@dataclass
class Team:
    title: str
    members: list = field(default_factory=list)

t = Team("альфа")
t.members.append("Аня")
print(t)
from dataclasses import dataclass

@dataclass
class Item:
    price: int
    count: int
    total: int = 0

    def __post_init__(self):
        self.total = self.price * self.count

print(Item(50, 3).total)
Проверь себя
0 / 6

1. Какие методы @dataclass генерирует по умолчанию?

2. Как правильно задать список как значение поля по умолчанию?

3. Что дают объекту параметры @dataclass(frozen=True)?

4. Когда вызывается метод __post_init__?

5. Сгенерированный dataclass-ом __eq__ сравнивает объекты…

6. Зачем в dataclass обязательны аннотации полей вроде name: str?

Карточки терминов
Запомнено: 0 / 7
Практика

Опишите @dataclass Book с полями title и author (строки) и pages (int, по умолчанию 100). Создайте две одинаковые книги «Евгений Онегин» Пушкина на 384 страницы и выведите результат их сравнения через ==, затем напечатайте первую книгу.

practice.py
Вопросы и ответы по уроку

Что такое @dataclass в Python простыми словами?

Это декоратор, который пишет за вас скучные методы класса-данных: по списку полей он генерирует конструктор __init__, печать __repr__ и сравнение __eq__. Вы описываете только поля с типами и значениями по умолчанию — как столбцы таблицы, а код методов Python собирает сам.

Урок 16: dataclass

Чем dataclass лучше обычного класса?

Меньше кода и меньше ошибок: не нужно переписывать в __init__ каждое поле, не нужно вручную поддерживать __repr__ и __eq__ при добавлении поля — декоратор перегенерирует их сам из описания. Обычный класс остаётся выбором, когда конструктору нужна сложная логика, которую неудобно прятать в __post_init__.

Почему нельзя написать items: list = [] в dataclass?

Изменяемое значение по умолчанию сделало бы список общим для всех объектов — классическая ловушка из урока про атрибуты класса. Декоратор перехватывает её заранее и поднимает ValueError. Правильно: items: list = field(default_factory=list) — для каждого экземпляра вызывается list() и создаётся свой список.

Как сделать объект dataclass неизменяемым?

Передать декоратору параметр frozen=True: присваивание полю после создания поднимет FrozenInstanceError. Бонус — у frozen-класса работает __hash__, поэтому его экземпляры можно класть в множества и использовать как ключи словаря, что обычному dataclass с eq=True недоступно.

Понравился урок? Сошлитесь на него

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.