W3docs

Python Dataclasses

Изучите Python dataclasses: декоратор @dataclass, значения по умолчанию field(), сортировку, неизменяемость и наследование с практическими примерами.

Dataclass — это обычный Python-класс, шаблонный код которого — __init__, __repr__ и __eq__ — генерируется автоматически декоратором @dataclass. Результат — меньше кода, меньше опечаток и классы, которые сразу же понятны при чтении.

В этой главе рассматривается:

  • Зачем нужны dataclasses и когда их использовать
  • Декоратор @dataclass
  • Значения по умолчанию для полей и вспомогательная функция field()
  • Управление равенством и сортировкой
  • Неизменяемые dataclasses с frozen=True
  • Логика после инициализации с __post_init__
  • Наследование в dataclasses
  • Dataclasses vs. NamedTuple vs. обычные классы

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

Зачем нужны dataclasses?

Рассмотрим класс, который хранит товар в интернет-магазине. Без dataclasses вы записываете одни и те же присваивания атрибутов трижды — один раз в __init__, один раз в __repr__ и один раз в __eq__:

class Product:
    def __init__(self, name, price, stock):
        self.name = name
        self.price = price
        self.stock = stock

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

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

Декоратор @dataclass генерирует всё вышеперечисленное из единственного аннотированного списка полей:

from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    stock: int

Обе версии ведут себя одинаково. Версия с dataclass короче, сложнее допустить в ней ошибку, и она сразу же сигнализирует, что этот класс является прежде всего контейнером данных.

Декоратор @dataclass

Импортируйте dataclass из стандартного модуля dataclasses и примените его к своему классу. Каждое поле объявляется как переменная класса с аннотацией типа:

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(1.5, 2.0)
print(p)          # Point(x=1.5, y=2.0)
print(p.x)        # 1.5

p2 = Point(1.5, 2.0)
print(p == p2)    # True  — __eq__ compares field by field

Декоратор генерирует:

МетодЧто делает
__init__Принимает каждое поле как параметр и присваивает его self
__repr__Возвращает читаемую строку вида Point(x=1.5, y=2.0)
__eq__Сравнивает два экземпляра поле за полем

Аннотации типов обязательны, но не проверяются во время выполнения

Объявления полей требуют аннотации типа (x: float). Python не проверяет тип во время выполнения — вы всё равно можете передать строку там, где ожидается float. Аннотация является метаданными, используемыми такими средствами проверки типов, как mypy, и самим механизмом dataclasses. Для проверки типов во время выполнения см. Python Type Hints.

Значения по умолчанию

Присвойте значение по умолчанию непосредственно полю, чтобы сделать его необязательным в __init__:

from dataclasses import dataclass

@dataclass
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False

c1 = Config()
print(c1)   # Config(host='localhost', port=8080, debug=False)

c2 = Config(host="example.com", port=443)
print(c2)   # Config(host='example.com', port=443, debug=False)

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

Изменяемые значения по умолчанию и field()

Нельзя использовать изменяемый объект (список, dict или set) в качестве простого значения по умолчанию. Python будет использовать один список для всех экземпляров, что приводит к трудноуловимым ошибкам:

from dataclasses import dataclass

# This raises a ValueError at class definition time:
# @dataclass
# class Bag:
#     items: list = []   # ValueError: mutable default is not allowed

Вместо этого используйте field(default_factory=...) для создания нового объекта для каждого экземпляра:

from dataclasses import dataclass, field

@dataclass
class Bag:
    items: list = field(default_factory=list)

b1 = Bag()
b2 = Bag()
b1.items.append("apple")

print(b1.items)   # ['apple']
print(b2.items)   # []  — b2 has its own separate list

default_factory принимает любой вызываемый объект без аргументов, включая лямбды и ваши собственные функции.

Вспомогательная функция field()

field() предоставляет детальный контроль над отдельными полями. Наиболее полезные параметры:

ПараметрНазначение
defaultПростое значение по умолчанию (только скаляры)
default_factoryВызываемый объект, создающий значение по умолчанию
reprFalse для исключения поля из __repr__
compareFalse для исключения поля из __eq__ (и сортировки)
initFalse для исключения поля из __init__
from dataclasses import dataclass, field
import time

@dataclass
class LogEntry:
    message: str
    level: str = "INFO"
    timestamp: float = field(default_factory=time.time, repr=False, compare=False)

entry = LogEntry("Server started")
print(entry)               # LogEntry(message='Server started', level='INFO')
# timestamp exists but is hidden from repr and ignored in comparisons
print(entry.timestamp > 0) # True

Сортировка

По умолчанию dataclasses поддерживают равенство (==, !=), но не сортировку (<, >, <=, >=). Включите сортировку, передав order=True декоратору:

from dataclasses import dataclass

@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int

v1 = Version(1, 2, 0)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 0)

print(v1 < v2)    # True
print(v1 == v3)   # True
print(v2 > v1)    # True

versions = [Version(2, 0, 0), Version(1, 9, 1), Version(1, 2, 3)]
print(sorted(versions))
# [Version(major=1, minor=2, patch=3),
#  Version(major=1, minor=9, patch=1),
#  Version(major=2, minor=0, patch=0)]

Python генерирует методы сравнения, сравнивая поля в порядке их объявления, как кортежи. Вы можете исключить поле из сравнений с помощью field(compare=False).

Неизменяемые dataclasses с frozen=True

Передайте frozen=True, чтобы все поля стали доступны только для чтения после создания. Любая попытка изменить поле вызовет ошибку FrozenInstanceError:

from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

london = Coordinate(51.5074, -0.1278)
print(london)        # Coordinate(lat=51.5074, lon=-0.1278)

# london.lat = 0.0  # FrozenInstanceError: cannot assign to field 'lat'

Замороженные dataclasses также являются хешируемыми (они реализуют __hash__), поэтому вы можете использовать их в качестве ключей словаря или элементов множества:

from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

cities = {
    Coordinate(51.5074, -0.1278): "London",
    Coordinate(48.8566,  2.3522): "Paris",
}
print(cities[Coordinate(51.5074, -0.1278)])   # London

Обычные (изменяемые) dataclasses не являются хешируемыми по умолчанию — Python устанавливает __hash__ в None, когда __eq__ определён без frozen=True.

Логика после инициализации с __post_init__

Иногда необходимо вычислить значение поля на основе других полей или провалидировать входные данные после выполнения __init__. Определите метод __post_init__ — он вызывается автоматически в конце сгенерированного __init__:

from dataclasses import dataclass, field
import math

@dataclass
class Circle:
    radius: float

    def __post_init__(self):
        if self.radius <= 0:
            raise ValueError(f"radius must be positive, got {self.radius}")

    @property
    def area(self):
        return math.pi * self.radius ** 2

c = Circle(5)
print(round(c.area, 4))   # 78.5398

# Circle(-1)  # ValueError: radius must be positive, got -1

Вы также можете вычислить производное поле. Пометьте его с помощью field(init=False), чтобы оно не появлялось в __init__, затем задайте его внутри __post_init__:

from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False, repr=True)

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

r = Rectangle(4, 6)
print(r)         # Rectangle(width=4, height=6, area=24)
print(r.area)    # 24

Наследование в dataclasses

Dataclass может наследоваться от другого dataclass. Метод __init__ дочернего класса включает поля обоих классов — поля родителя первыми, в порядке их объявления:

from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    age: int

@dataclass
class Dog(Animal):
    breed: str

rex = Dog(name="Rex", age=3, breed="Labrador")
print(rex)    # Dog(name='Rex', age=3, breed='Labrador')

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

from dataclasses import dataclass

@dataclass
class Animal:
    name: str
    age: int = 0   # has a default

# @dataclass
# class Dog(Animal):
#     breed: str   # TypeError: non-default argument 'breed' follows default argument

Обходное решение — задать значение по умолчанию для поля дочернего класса или реструктурировать иерархию так, чтобы поля со значениями по умолчанию шли последними.

Параметры декоратора: краткий обзор

@dataclass(
    init=True,     # generate __init__       (default True)
    repr=True,     # generate __repr__       (default True)
    eq=True,       # generate __eq__         (default True)
    order=False,   # generate <, >, <=, >=   (default False)
    frozen=False,  # make fields immutable   (default False)
)
class MyClass:
    ...

Большинство из них редко нужно менять. Чаще всего используются order=True и frozen=True.

Вспомогательные функции

Модуль dataclasses также предоставляет три полезные функции:

fields()

Возвращает кортеж объектов Field, описывающих каждое поле класса:

from dataclasses import dataclass, fields

@dataclass
class Point:
    x: float
    y: float

for f in fields(Point):
    print(f.name, f.type)
# x <class 'float'>
# y <class 'float'>

asdict()

Преобразует экземпляр dataclass в обычный словарь (рекурсивно):

from dataclasses import dataclass, asdict

@dataclass
class Address:
    street: str
    city: str

@dataclass
class Person:
    name: str
    address: Address

p = Person("Alice", Address("10 Downing St", "London"))
print(asdict(p))
# {'name': 'Alice', 'address': {'street': '10 Downing St', 'city': 'London'}}

Это полезно при сериализации в JSON или отправке данных в API.

astuple()

Преобразует в кортеж (рекурсивно):

from dataclasses import dataclass, astuple

@dataclass
class Point:
    x: float
    y: float

p = Point(3.0, 4.0)
print(astuple(p))   # (3.0, 4.0)

Dataclasses vs. NamedTuple vs. обычные классы

ВозможностьОбычный классNamedTupledataclass
Авто __init__НетДаДа
Авто __repr__НетДаДа
Авто __eq__НетДа (по значению)Да (по значению)
ИзменяемостьДаНетДа (по умолчанию)
ХешируемостьНет (если определён __eq__)ДаТолько с frozen=True
СортировкаВручнуюДаorder=True
НаследованиеДаОграниченноДа
Проверка isinstanceДаДа (также tuple)Да
Распаковка (a, b = obj)НетДаНет

Используйте dataclass, когда:

  • Нужны изменяемые данные с возможностью неизменяемости.
  • Нужно наследование или логика после инициализации.
  • Нужен детальный контроль над полями (field()).

Используйте NamedTuple, когда:

  • Нужна неизменяемая запись, которая также ведёт себя как кортеж (позиционная распаковка, строки CSV).
  • Нужна совместимость с кодом, ожидающим кортежи.

Используйте обычный класс, когда:

  • Класс имеет значительное поведение и очень мало данных.
  • Нужен пользовательский __init__, который нельзя выразить через __post_init__.

Распространённые ошибки

Изменяемые значения по умолчанию. Использование списка или dict в качестве простого значения по умолчанию вызывает ValueError во время определения класса. Всегда используйте field(default_factory=...).

Хешируемость. Обычные dataclasses не являются хешируемыми. Если они нужны как ключи словаря или в множествах, используйте frozen=True или передайте unsafe_hash=True (не рекомендуется).

eq=False. Если отключить генерацию равенства (eq=False), Python перейдёт к сравнению по идентичности (is), что почти никогда не является желаемым поведением для объектов данных.

Порядок значений по умолчанию при наследовании. Если поле родителя имеет значение по умолчанию, а поле дочернего класса — нет, Python вызовет TypeError. Тщательно планируйте порядок полей в вашей иерархии.

Резюме

КонцепцияЧто делает
@dataclassАвтоматически генерирует __init__, __repr__, __eq__
field()Детальный контроль над полями: значения по умолчанию, repr, compare, init
default_factoryСоздаёт новое изменяемое значение по умолчанию для каждого экземпляра
order=TrueДобавляет <, >, <=, >= на основе порядка полей
frozen=TrueДелает поля доступными только для чтения, а экземпляр хешируемым
__post_init__Выполняется после __init__ для валидации или вычисления производных полей
fields()Возвращает метаданные о каждом поле
asdict()Преобразует экземпляр в обычный dict (рекурсивно)
astuple()Преобразует экземпляр в обычный кортеж (рекурсивно)

По связанным темам см. классы и объекты Python, наследование в Python и абстрактные базовые классы Python.

Практика

Практика
Which decorator do you use to create a dataclass in Python?
Which decorator do you use to create a dataclass in Python?
Was this page helpful?