Python Dataclasses
Изучите Python dataclasses: декоратор @dataclass, значения по умолчанию field(), сортировку, неизменяемость и наследование с практическими примерами.
Dataclass — это обычный Python-класс, шаблонный код которого — __init__, __repr__ и __eq__ — генерируется автоматически декоратором @dataclass. Результат — меньше кода, меньше опечаток и классы, которые сразу же понятны при чтении.
В этой главе рассматривается:
- Зачем нужны dataclasses и когда их использовать
- Декоратор
@dataclass - Значения по умолчанию для полей и вспомогательная функция
field() - Управление равенством и сортировкой
- Неизменяемые dataclasses с
frozen=True - Логика после инициализации с
__post_init__ - Наследование в dataclasses
- Dataclasses vs.
NamedTuplevs. обычные классы
Прежде чем читать эту главу, убедитесь, что вы хорошо знакомы с классами и объектами 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 listdefault_factory принимает любой вызываемый объект без аргументов, включая лямбды и ваши собственные функции.
Вспомогательная функция field()
field() предоставляет детальный контроль над отдельными полями. Наиболее полезные параметры:
| Параметр | Назначение |
|---|---|
default | Простое значение по умолчанию (только скаляры) |
default_factory | Вызываемый объект, создающий значение по умолчанию |
repr | False для исключения поля из __repr__ |
compare | False для исключения поля из __eq__ (и сортировки) |
init | False для исключения поля из __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. обычные классы
| Возможность | Обычный класс | NamedTuple | dataclass |
|---|---|---|---|
Авто __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.