Python · Синтаксис · Промежуточный
dataclasses
Научитесь использовать датаклассы Python для определения чистых контейнеров данных без шаблонного кода с автоматической генерацией __init__, __repr__ и __eq__.
Краткое введение в тему и пояснения перед упражнениями (упражнения ниже):
Frozen, Order и наследование
#Справочник: dataclasses
#Упражнения:
Базовый датакласс
#Создайте датакласс `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
Изменяемый 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) # []
Валидация в __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)
Вычисляемое поле
#Создайте датакласс `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)
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
Датакласс с упорядочением
#Создайте датакласс `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)
Вложенные датаклассы
#Создайте два датакласса: `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))
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
Датакласс в 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))
Система инвентаря
#Разработайте небольшую систему инвентаря с датаклассами: - `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