Перейти к содержимому
Теория · Классы · урок 9 из 11

Классы данных

Представьте, что на каждом новом чертеже вы рисуете одни и те же части: как заполнить карточку, как её подписать, как сравнить две. Меняются только названия полей. Удобнее перечислить поля, а одинаковые части пусть дорисует помощник.

Именно так выглядит класс, который в основном хранит данные. Класс Point с методами __init__, __repr__ и __eq__ занимает десяток строк, и имена x и y повторяются в каждом из трёх методов. Добавьте третью координату — и править придётся все три. Для таких классов эти методы пишет стандартная библиотека:

Python
from dataclasses import dataclass


@dataclass
class Point:
    x: int
    y: int = 0


p = Point(1, 2)
print(p)  # Point(x=1, y=2)
print(p == Point(1, 2), Point(5))  # True Point(x=5, y=0)

Импорт одного имени

Модули вы уже подключали целиком. Строка import math подключает модуль math, и его имена пишут через точку: math.isclose.

Строка from dataclasses import dataclass работает иначе. Она берёт из модуля dataclasses одно имя dataclass, и дальше его пишут без имени модуля.

Декоратор для класса

dataclass — декоратор, только принимает он не функцию, а класс. Строка @dataclass над классом работает так же, как над def: она равна присваиванию Point = dataclass(Point) сразу после тела класса.

Декоратор дописывает в класс методы и возвращает тот же класс. Класс, к которому его применили, называют классом данных.

Поля и аннотации

Запись x: int — аннотация: имя, двоеточие и тип. В классе данных каждое имя с аннотацией в теле класса становится полем. Декоратор читает поля по порядку и пишет три метода:

  • __init__ с такими же параметрами;
  • __repr__ в виде Point(x=1, y=2);
  • __eq__, который сравнивает поля по порядку.

Давайте разберём, что ещё видно в примере:

  1. Значение после = становится значением по умолчанию: Point(5) создаёт точку с y, равным 0.
  2. Аннотацию Python не проверяет: Point("a") тоже создаст точку. Тип в ней пишут для читателя.
  3. Сравнение == даёт True только для экземпляров того же класса: Point(1, 2) == (1, 2) — это False.

Здесь аналогия с помощником заканчивается. Декоратор ничего не рисует на глазах: он один раз, при создании класса, дописывает в него обычные методы. Дальше класс работает так, будто вы написали их сами.

Класс данных — обычный класс

__init__ класса данных записывает каждое поле в атрибут экземпляра. Поэтому у каждой точки свои x и y, даже если у поля есть значение в теле класса. После p.x = 10 у точки новая координата. Методы к классу данных добавляют так же, как к любому классу:

Python
@dataclass
class Item:
    name: str
    price: float
    qty: int = 1

    def total(self):
        return self.price * self.qty


cart = [Item("pen", 1.5, 4), Item("book", 12.0)]
print(sum(item.total() for item in cart))  # 18.0
print(cart[1])  # Item(name='book', price=12.0, qty=1)

__repr__ показывает значения полей через их собственный __repr__, поэтому строка 'book' стоит в кавычках.

Частая ошибка

Поле без значения по умолчанию пишут после поля со значением:

Python
@dataclass
class Item:
    name: str
    qty: int = 1
    price: float  # ✗
TEXT
TypeError: non-default argument 'price' follows default argument

В __init__ параметр без значения по умолчанию не может стоять после параметра со значением — как и в обычной функции. Ошибка возникает при создании класса, ещё до первого экземпляра. Перенесите поля со значениями по умолчанию в конец.

Главное

  • @dataclass пишет __init__, __repr__ и __eq__ по полям класса.
  • Поле — это имя с аннотацией в теле класса; значение после = — значение по умолчанию.
  • Аннотацию Python не проверяет, а поля без значения по умолчанию идут первыми.
  • from dataclasses import dataclass берёт из модуля одно имя, и его пишут без имени модуля.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом