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