# Домашнє завдання · Тема 25 · Класи й обʼєкти

> Лекція: [lecture.html](lecture.html) · Практика: [practice.ipynb](practice.ipynb) · Тест: [quiz.html](quiz.html)

Усі три рівні — про одну програму: маленький **облік бібліотеки**. Наскрізний приклад
лекції (продажі) ми лишаємо позаду навмисно: клас треба вміти описати для своєї
предметної області, а не лише повторити чужий.

Працюй у звичайному файлі `biblioteka.py`, а перевірки пиши поруч, унизу того самого
файлу, під захисною умовою:

```python
if __name__ == "__main__":
    # тут усі assert-и
    ...
```

Тоді файл можна і запускати (`python3 biblioteka.py`), і імпортувати з іншого місця.
Якщо жоден `assert` не спрацював, програма закінчується мовчки — це і є «зроблено».

---

## 🟢 Рівень 1 — База

Напиши клас `Книга` з полями `назва`, `автор`, `рік`, `примірників` і методом
`доступна()`, що повертає `True`, якщо примірників більше нуля.

Обовʼязково:

- усі перевірки — в `__init__`, **до** присвоєнь: порожня назва, рік менший за 1450
  або більший за 2100, відʼємна кількість примірників — це `ValueError` із
  повідомленням українською;
- `__repr__`, з якого видно назву, автора й рік;
- жодного змінюваного значення в тілі класу.

**Зроблено, якщо** цей блок перевірок проходить без жодного спрацювання:

```python
книга = Книга("Кобзар", "Шевченко", 1840, 3)
assert книга.доступна() is True
assert repr(книга) == "Книга('Кобзар', 'Шевченко', 1840)"   # свій формат — свій рядок

for погані in [("", "Шевченко", 1840, 3), ("Кобзар", "Шевченко", 1200, 3),
               ("Кобзар", "Шевченко", 1840, -1)]:
    try:
        Книга(*погані)
    except ValueError:
        pass
    else:
        raise AssertionError(f"перевірка не спрацювала: {погані}")
```

---

## 🟡 Рівень 2 — Плюс

Додай клас `Бібліотека`, який зберігає книжки й **веде журнал подій**.

1. У `__init__` створи два поля: `self.книжки = []` і `self.журнал = []`.
2. Метод `додати(книга)` — з перевіркою `isinstance(книга, Книга)`, інакше `TypeError`.
   Кожне додавання пише рядок у `self.журнал`.
3. Метод `видати(назва)` — зменшує `примірників` на одиницю й повертає `True`; якщо
   книжки немає або примірників нуль, повертає `False`. Обидва випадки — у журнал.
4. Атрибут класу `максимум_на_руки = 5` (незмінне число!) і метод, який ним
   користується.
5. `__repr__` для бібліотеки: скільки назв і скільки примірників усього.

**Зроблено, якщо:**

- дві бібліотеки не бачать журналів одна одної — це доводить `assert`:

  ```python
  а, б = Бібліотека(), Бібліотека()
  а.додати(Книга("Кобзар", "Шевченко", 1840, 1))
  assert len(а.журнал) == 1 and len(б.журнал) == 0
  assert а.журнал is not б.журнал
  ```

- `а.видати("Кобзар")` двічі поспіль дає спершу `True`, потім `False`, і в журналі
  зʼявилось два різні рядки;
- `а.додати("Кобзар")` (рядок замість книжки) кидає `TypeError`;
- після `а.максимум_на_руки = 2` значення в самому класі лишається `5`. Перевір це
  `assert Бібліотека.максимум_на_руки == 5` і поясни в коментарі одним реченням, чому
  так сталося.

---

## 🔴 Рівень 3 — Виклик

Три задачі. Кожну можна зробити незалежно.

**А. Пастка своїми руками.** Свідомо зроби помилку: перенеси `журнал = []` з `__init__`
у тіло класу `Бібліотека`. Напиши **тест, який цю помилку ловить**, — такий, що падає на
зіпсованій версії й проходить на правильній. Поверни правильний варіант і залиш тест
у файлі.

**Б. Порахуй ціну гнучкості.** Створи 200 000 обʼєктів `Книга` і заміряй памʼять
(`tracemalloc.get_traced_memory()` до і після). Потім додай у клас
`__slots__ = ("назва", "автор", "рік", "примірників")` і повтори замір.

**В. Кому цей клас не потрібен.** Знайди у власному коді (або напиши навмисно) клас,
який складається лише з `__init__` і методів `отримати_X` / `встановити_X`. Перепиши той
самий сенс без класу.

**Зроблено, якщо:**

- **А:** тест справді розрізняє дві версії — покажи це, запустивши його двічі й
  зафіксувавши результат у коментарі («на версії з журналом у тілі класу падає на рядку
  N»);
- **Б:** маєш два числа й відношення між ними, а також рядок, який доводить, що після
  `__slots__` одруківка більше не проходить мовчки:

  ```python
  try:
      книга.примірникив = 3      # одруківка
  except AttributeError:
      print("✅ __slots__ ловить одруківку")
  ```

- **В:** нова версія коротша за стару, і ти можеш назвати одним реченням, що саме
  зникло разом із класом (і чого не зникло).

---

## Підказки

- Перевірки в `__init__` пиши **до** першого присвоєння. Якщо половину полів уже
  записано, а потім кинуто виняток, обʼєкт нікуди не потрапить — але звичка ставити
  перевірки першими рятує від напівзаповнених обʼєктів у складніших випадках.
- Не можеш вирішити, метод це чи функція? Спитай себе: чи потрібен йому `self`. Якщо
  всередині жодного разу не згадується `self`, це, найімовірніше, звичайна функція.
- Найшвидший спосіб побачити, що насправді лежить в обʼєкті, — надрукувати
  `vars(обʼєкт)`. Це той самий `__dict__`, тільки коротше писати.
- Щоб перевірити, чи атрибут спільний, не рахуй елементи — питай про ідентичність:
  `а.журнал is б.журнал`. Довжини можуть збігтися випадково, `is` не збіжиться ніколи.
- Формат `__repr__` вибираєш ти сам — але візьми за орієнтир рядок, схожий на виклик,
  яким такий обʼєкт можна створити знову. Тоді свій же `assert` на `repr` писати легко.
