Python · Синтаксис · Промежуточный

dataclasses

10 задач

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

@dataclass: автоматический шаблонный код

#
До датаклассов создание простого контейнера данных в Python требовало ручного написания `__init__`, `__repr__` и `__eq__` — много повторяющегося кода. Декоратор `@dataclass` (Python 3.7+) генерирует всё это автоматически. ```python from dataclasses import dataclass, field ``` ## Проблема без датаклассов ```python # Старый способ: много шаблонного кода class Point: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Point(x={self.x}, y={self.y})" def __eq__(self, other): return isinstance(other, Point) and self.x == other.x and self.y == other.y ``` ## Решение с dataclass ```python @dataclass class Point: x: float y: float p1 = Point(1.0, 2.0) p2 = Point(1.0, 2.0) print(p1) # Point(x=1.0, y=2.0) — __repr__ авто-генерируется print(p1 == p2) # True — __eq__ авто-генерируется print(p1.x) # 1.0 ``` Декоратор генерирует `__init__`, `__repr__` и `__eq__` на основе аннотаций класса. ## Значения по умолчанию ```python @dataclass class Config: host: str = "localhost" port: int = 8080 debug: bool = False c = Config() # все по умолчанию print(c) # Config(host='localhost', port=8080, debug=False) c2 = Config(host="0.0.0.0", port=443) print(c2) # Config(host='0.0.0.0', port=443, debug=False) ``` **Правило**: поля со значениями по умолчанию должны идти после полей без них (то же правило, что и для параметров функции). ## field() для изменяемых значений по умолчанию Нельзя использовать `default=[]` напрямую — Python совместно использовал бы тот же список для всех экземпляров. Вместо этого используйте `field(default_factory=list)`: ```python @dataclass class Student: name: str grades: list = field(default_factory=list) # каждый экземпляр получает свой список active: bool = True s1 = Student("Alice") s2 = Student("Bob") s1.grades.append(95) print(s1.grades) # [95] print(s2.grades) # [] — независимый! ``` ## __post_init__: вычисляемые поля и валидация ```python @dataclass class Rectangle: width: float height: float area: float = field(init=False) # исключено из __init__ def __post_init__(self): if self.width <= 0 or self.height <= 0: raise ValueError("Ширина и высота должны быть положительными") self.area = self.width * self.height r = Rectangle(4.0, 5.0) print(r) # Rectangle(width=4.0, height=5.0, area=20.0) print(r.area) # 20.0 ``` `__post_init__` выполняется автоматически после `__init__`.

Frozen, Order и наследование

#
## frozen=True: неизменяемые датаклассы ```python from dataclasses import dataclass @dataclass(frozen=True) class Color: r: int g: int b: int red = Color(255, 0, 0) print(red) # Color(r=255, g=0, b=0) # Замороженные экземпляры хешируемы — можно как ключ словаря или в множестве! palette = {red, Color(0, 255, 0), Color(0, 0, 255)} config = {Color(255, 0, 0): "red", Color(0, 0, 255): "blue"} # Попытка изменить бросает FrozenInstanceError: red.r = 100 # FrozenInstanceError: cannot assign to field 'r' ``` ## order=True: поддержка сравнения ```python @dataclass(order=True) class Version: major: int minor: int patch: int v1 = Version(1, 2, 3) v2 = Version(1, 3, 0) v3 = Version(2, 0, 0) print(v1 < v2) # True — сравнивает поле за полем print(v2 < v3) # True versions = [v3, v1, v2] print(sorted(versions)) # [Version(major=1, minor=2, patch=3), Version(major=1, minor=3, patch=0), Version(major=2, minor=0, patch=0)] ``` ## Наследование ```python @dataclass class Animal: name: str sound: str @dataclass class Dog(Animal): breed: str sound: str = "woof" # переопределение по умолчанию d = Dog(name="Rex", breed="Labrador") print(d) # Dog(name='Rex', sound='woof', breed='Labrador') print(d.sound) # woof ``` ## Вложенные датаклассы ```python @dataclass class Address: street: str city: str country: str = "Ukraine" @dataclass class Person: name: str age: int address: Address p = Person( name="Alice", age=30, address=Address("ул. Крещатик", "Киев"), ) print(p) # Person(name='Alice', age=30, address=Address(street='ул. Крещатик', city='Киев', country='Ukraine')) print(p.address.city) # Киев ``` ## Конвертация в dict и tuple ```python from dataclasses import dataclass, asdict, astuple @dataclass class Point: x: float y: float p = Point(1.5, 2.5) d = asdict(p) # {"x": 1.5, "y": 2.5} t = astuple(p) # (1.5, 2.5) # Удобно для сериализации в JSON import json json_str = json.dumps(asdict(p)) print(json_str) # {"x": 1.5, "y": 2.5} ```

Справочник: dataclasses

#
## Параметры @dataclass ```python @dataclass( init=True, # генерировать __init__ repr=True, # генерировать __repr__ eq=True, # генерировать __eq__ и __ne__ order=False, # генерировать __lt__, __le__, __gt__, __ge__ unsafe_hash=False, # генерировать __hash__ даже если eq=True frozen=False, # сделать экземпляры неизменяемыми slots=False, # использовать __slots__ (Python 3.10+) kw_only=False, # все поля keyword-only в __init__ (Python 3.10+) ) class MyClass: ... ``` ## Параметры field() ```python from dataclasses import field @dataclass class Example: name: str # обязательное поле score: float = 0.0 # простое значение по умолчанию tags: list = field(default_factory=list) # изменяемое значение по умолчанию _id: int = field(init=False, repr=False) # исключено из init и repr label: str = field(default="n/a", compare=False) # исключено из eq ``` | Параметр | По умолч. | Значение | |---|---|---| | `default` | MISSING | Значение по умолчанию | | `default_factory` | MISSING | Callable возвращающий значение по умолч. | | `init` | True | Включать в `__init__` | | `repr` | True | Включать в `__repr__` | | `hash` | None | Включать в `__hash__` | | `compare` | True | Включать в `__eq__` и упорядочение | | `metadata` | None | Произвольный словарь метаданных | ## Вспомогательные функции | Функция | Что делает | |---|---| | `asdict(obj)` | Конвертировать во вложенный словарь | | `astuple(obj)` | Конвертировать во вложенный кортеж | | `fields(class_or_instance)` | Вернуть кортеж объектов Field | | `replace(obj, **changes)` | Вернуть новый экземпляр с некоторыми изменениями | ## dataclass vs обычный класс vs namedtuple | Функция | обычный класс | namedtuple | dataclass | |---|---|---|---| | Авто `__init__` | Нет | Да | Да | | Авто `__repr__` | Нет | Да | Да | | Авто `__eq__` | Нет | Да | Да | | Мутабельный | Да | Нет | Да (по умолч.) | | Хешируемый | Нет | Да | С frozen=True | | Упорядочение | Вручную | Да | С order=True | | Методы | Да | Ограничено | Да |
01

Базовый датакласс

#

Создайте датакласс `Book` с полями: `title: str`, `author: str`, `year: int`, `rating: float = 0.0`. Затем создайте два экземпляра: `Book("1984", "Orwell", 1949, 9.5)` и `Book("Dune", "Herbert", 1965)`. Выведите оба и проверьте что `==` работает (создайте два одинаковых объекта Book и проверьте что они равны).

from dataclasses import dataclass


@dataclass
class Book:
    pass


b1 = Book("1984", "Orwell", 1949, 9.5)
b2 = Book("Dune", "Herbert", 1965)
print(b1)
print(b2)

b3 = Book("1984", "Orwell", 1949, 9.5)
print(b1 == b3)   # True
Решение
from dataclasses import dataclass


@dataclass
class Book:
    title: str
    author: str
    year: int
    rating: float = 0.0


b1 = Book("1984", "Orwell", 1949, 9.5)
b2 = Book("Dune", "Herbert", 1965)
print(b1)
print(b2)

b3 = Book("1984", "Orwell", 1949, 9.5)
print(b1 == b3)   # True
02

Изменяемый default через field()

#

Создайте датакласс `Team` с полями `name: str` и `members: list`, где `members` по умолчанию — пустой список. Покажите что каждый экземпляр получает свой независимый список: создайте две команды, добавьте участника в первую и проверьте что вторая остаётся пустой. Используйте `field(default_factory=list)`.

from dataclasses import dataclass, field


@dataclass
class Team:
    name: str
    members: list = ???   # исправьте это


t1 = Team("Alpha")
t2 = Team("Beta")
t1.members.append("Alice")
print(t1.members)   # ["Alice"]
print(t2.members)   # []  <- должен быть пустым
Решение
from dataclasses import dataclass, field


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


t1 = Team("Alpha")
t2 = Team("Beta")
t1.members.append("Alice")
print(t1.members)   # ["Alice"]
print(t2.members)   # []
03

Валидация в __post_init__

#

Создайте датакласс `Temperature` с одним полем `celsius: float`. В `__post_init__` проверьте что температура выше абсолютного нуля (-273.15°C) и бросайте `ValueError` с сообщением `"Temperature below absolute zero"` если нет. Протестируйте оба случая: валидный и невалидный.

from dataclasses import dataclass


@dataclass
class Temperature:
    celsius: float

    def __post_init__(self):
        pass   # валидация здесь


print(Temperature(100))
print(Temperature(-273))
Temperature(-300)   # raises ValueError
Решение
from dataclasses import dataclass


@dataclass
class Temperature:
    celsius: float

    def __post_init__(self):
        if self.celsius < -273.15:
            raise ValueError("Temperature below absolute zero")


print(Temperature(100))
print(Temperature(-273))
try:
    Temperature(-300)
except ValueError as e:
    print(e)
04

Вычисляемое поле

#

Создайте датакласс `Circle` с полем `radius: float` и вычисляемым полем `area: float`, которое НЕ является частью `__init__` (используйте `field(init=False)`). Вычислите `area` в `__post_init__` как `math.pi * radius ** 2`. Выведите Circle и покажите что `area` включено в repr.

from dataclasses import dataclass, field
import math


@dataclass
class Circle:
    radius: float
    area: float = field(init=False)

    def __post_init__(self):
        pass   # вычислите area здесь


c = Circle(5)
print(c)
print(c.area)
Решение
from dataclasses import dataclass, field
import math


@dataclass
class Circle:
    radius: float
    area: float = field(init=False)

    def __post_init__(self):
        self.area = math.pi * self.radius ** 2


c = Circle(5)
print(c)
print(c.area)
05

Frozen датакласс как ключ словаря

#

Создайте замороженный датакласс `Coordinate` с полями `lat: float` и `lon: float`. Продемонстрируйте что: 1. Нельзя изменить экземпляр `Coordinate` (покажите ошибку) 2. Экземпляры `Coordinate` можно использовать как ключи словаря — создайте словарь отображающий координаты на названия мест.

from dataclasses import dataclass


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


c = Coordinate(50.45, 30.52)

try:
    c.lat = 0
except Exception as e:
    print(e)

locations = {c: "Kyiv"}
print(locations[Coordinate(50.45, 30.52)])   # Kyiv
Решение
from dataclasses import dataclass


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


c = Coordinate(50.45, 30.52)

try:
    c.lat = 0
except Exception as e:
    print(type(e).__name__, e)

locations = {c: "Kyiv"}
print(locations[Coordinate(50.45, 30.52)])   # Kyiv
06

Датакласс с упорядочением

#

Создайте датакласс `Student` с `order=True` и полями: `grade: float`, `name: str`. Создайте список из 5 студентов с разными оценками и именами, отсортируйте его (первичная сортировка по `grade`) и выведите результат. Обратите внимание: упорядочение сравнивает поля в порядке объявления — поставьте `grade` первым.

from dataclasses import dataclass


@dataclass(order=???)
class Student:
    grade: float
    name: str


students = [
    Student(3.5, "Alice"),
    Student(4.0, "Bob"),
    Student(3.5, "Charlie"),
    Student(2.8, "Dave"),
    Student(4.0, "Eve"),
]
print(sorted(students))
Решение
from dataclasses import dataclass


@dataclass(order=True)
class Student:
    grade: float
    name: str


students = [
    Student(3.5, "Alice"),
    Student(4.0, "Bob"),
    Student(3.5, "Charlie"),
    Student(2.8, "Dave"),
    Student(4.0, "Eve"),
]
for s in sorted(students):
    print(s)
07

Вложенные датаклассы

#

Создайте два датакласса: `Address` (поля: `street: str`, `city: str`, `country: str = "Ukraine"`) и `Person` (поля: `name: str`, `age: int`, `address: Address`). Создайте экземпляр `Person` с вложенным `Address`, выведите его и получите доступ к вложенному городу. Затем используйте `asdict()` для конвертации Person в словарь.

from dataclasses import dataclass, asdict


@dataclass
class Address:
    street: str
    city: str
    country: str = "Ukraine"


@dataclass
class Person:
    name: str
    age: int
    address: Address


p = Person("Alice", 30, Address("Main St", "Kyiv"))
print(p)
print(p.address.city)
print(asdict(p))
Решение
from dataclasses import dataclass, asdict


@dataclass
class Address:
    street: str
    city: str
    country: str = "Ukraine"


@dataclass
class Person:
    name: str
    age: int
    address: Address


p = Person("Alice", 30, Address("Main St", "Kyiv"))
print(p)
print(p.address.city)   # Kyiv
print(asdict(p))
08

replace() для создания вариантов

#

Создайте замороженный датакласс `Config` с полями `host: str = "localhost"`, `port: int = 8080`, `debug: bool = False`. Используйте `dataclasses.replace()` для создания production-конфигурации из development-конфигурации путём изменения `host` и `debug`. Покажите что оригинал не изменился, а новый экземпляр содержит обновлённые значения.

from dataclasses import dataclass, replace


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


dev = Config(debug=True)
prod = replace(dev, ???)   # измените host и debug
print(dev)
print(prod)
print(dev is prod)   # False
Решение
from dataclasses import dataclass, replace


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


dev = Config(debug=True)
prod = replace(dev, host="prod.example.com", debug=False)
print(dev)    # Config(host="localhost", port=8080, debug=True)
print(prod)   # Config(host="prod.example.com", port=8080, debug=False)
print(dev is prod)   # False
09

Датакласс в JSON

#

Создайте датакласс `Product` с полями `name: str`, `price: float`, `in_stock: bool = True` и `tags: list = field(default_factory=list)`. Создайте 3 экземпляра продуктов, конвертируйте каждый в словарь с помощью `asdict()`, затем сериализуйте список словарей в строку JSON с помощью `json.dumps()`. Выведите результат.

from dataclasses import dataclass, field, asdict
import json


@dataclass
class Product:
    name: str
    price: float
    in_stock: bool = True
    tags: list = field(default_factory=list)


products = [
    Product("Apple", 0.99, tags=["fruit", "fresh"]),
    Product("Laptop", 999.0, tags=["electronics"]),
    Product("Pen", 1.5, in_stock=False),
]
# Конвертируйте в JSON и выведите
Решение
from dataclasses import dataclass, field, asdict
import json


@dataclass
class Product:
    name: str
    price: float
    in_stock: bool = True
    tags: list = field(default_factory=list)


products = [
    Product("Apple", 0.99, tags=["fruit", "fresh"]),
    Product("Laptop", 999.0, tags=["electronics"]),
    Product("Pen", 1.5, in_stock=False),
]
data = [asdict(p) for p in products]
print(json.dumps(data, indent=2))
10

Система инвентаря

#

Разработайте небольшую систему инвентаря с датаклассами: - `Category` (frozen, поля: `name: str`, `tax_rate: float = 0.0`) - `Item` (поля: `name: str`, `price: float`, `quantity: int`, `category: Category`) - `Inventory` (поле: `items: list = field(default_factory=list)`, метод `add_item(item)`, метод `total_value()` возвращающий сумму price*quantity для всех товаров) Создайте несколько товаров и выведите общую стоимость инвентаря.

from dataclasses import dataclass, field


@dataclass(frozen=True)
class Category:
    name: str
    tax_rate: float = 0.0


@dataclass
class Item:
    name: str
    price: float
    quantity: int
    category: Category


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

    def add_item(self, item):
        pass

    def total_value(self):
        pass


inv = Inventory()
food = Category("Food", 0.05)
tech = Category("Tech", 0.2)
inv.add_item(Item("Apple", 1.0, 100, food))
inv.add_item(Item("Laptop", 800.0, 5, tech))
print(inv.total_value())   # 4100.0
Решение
from dataclasses import dataclass, field


@dataclass(frozen=True)
class Category:
    name: str
    tax_rate: float = 0.0


@dataclass
class Item:
    name: str
    price: float
    quantity: int
    category: Category


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

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

    def total_value(self):
        return sum(item.price * item.quantity for item in self.items)


inv = Inventory()
food = Category("Food", 0.05)
tech = Category("Tech", 0.2)
inv.add_item(Item("Apple", 1.0, 100, food))
inv.add_item(Item("Laptop", 800.0, 5, tech))
print(inv.total_value())   # 4100.0