Классы данных
Представьте, что на каждом новом чертеже вы рисуете одни и те же части: как заполнить карточку, как её подписать, как сравнить две. Меняются только названия полей. Удобнее перечислить поля, а одинаковые части пусть дорисует помощник.
Именно так выглядит класс, который в основном хранит данные. Класс Point с методами __init__, __repr__ и __eq__ занимает десяток строк, и имена x и y повторяются в каждом из трёх методов. Добавьте третью координату — и править придётся все три. Для таких классов эти методы пишет стандартная библиотека:
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__, который сравнивает поля по порядку.
Давайте разберём, что ещё видно в примере:
- Значение после
=становится значением по умолчанию:Point(5)создаёт точку сy, равным0. - Аннотацию Python не проверяет:
Point("a")тоже создаст точку. Тип в ней пишут для читателя. - Сравнение
==даётTrueтолько для экземпляров того же класса:Point(1, 2) == (1, 2)— этоFalse.
Здесь аналогия с помощником заканчивается. Декоратор ничего не рисует на глазах: он один раз, при создании класса, дописывает в него обычные методы. Дальше класс работает так, будто вы написали их сами.
Класс данных — обычный класс
__init__ класса данных записывает каждое поле в атрибут экземпляра. Поэтому у каждой точки свои x и y, даже если у поля есть значение в теле класса. После p.x = 10 у точки новая координата. Методы к классу данных добавляют так же, как к любому классу:
@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' стоит в кавычках.
Частая ошибка
Поле без значения по умолчанию пишут после поля со значением:
@dataclass
class Item:
name: str
qty: int = 1
price: float # ✗TypeError: non-default argument 'price' follows default argumentВ __init__ параметр без значения по умолчанию не может стоять после параметра со значением — как и в обычной функции. Ошибка возникает при создании класса, ещё до первого экземпляра. Перенесите поля со значениями по умолчанию в конец.
Главное
@dataclassпишет__init__,__repr__и__eq__по полям класса.- Поле — это имя с аннотацией в теле класса; значение после
=— значение по умолчанию. - Аннотацию Python не проверяет, а поля без значения по умолчанию идут первыми.
from dataclasses import dataclassберёт из модуля одно имя, и его пишут без имени модуля.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.