У програмі є словник {"товар": "кава", "кількість": 3, "ціна": 90.0} і функція сума(продаж), що множить кількість на ціну. Що саме змінить перехід на клас Продаж?
Чому так. Число клас не змінює: 3 × 90.0 = 270.0 обома способами (інтерактив 1, дія «порахувати суму»). Змінюється те, ДЕ живе знання: __init__ — єдиний вхід, тому перевірка «кількість > 0» пишеться один раз, а метод сума() лежить у тому самому класі. Швидшим і компактнішим звичайний клас не стає — навпаки, кожен примірник тягне власний __dict__; і типи Python сам не перевіряє.
Питання 2 / 8
Чому __init__ точніше називати ініціалізатором, а не конструктором?
Чому так. Подивись на кадр «обʼєкт уже є, полів 0» в інтерактиві 2: примірник уже існує, уже має тип Продаж і власну адресу — а __dict__ у нього порожній. Саме цей готовий обʼєкт приїжджає в self, і __init__ його заповнює. Власне створенням займається окремий метод __new__. Те, що __init__ не обовʼязковий, — правда, але до слова «конструктор» це відношення не має.
Питання 3 / 8
p = Продаж("кава", 3, 90.0). Що станеться при виклику Продаж.сума() — без аргументів?
Чому так. сума — звичайна функція, що лежить у класі, і її перший параметр зветься self. Крапка в p.сума() підставляє обʼєкт автоматично; Продаж.сума(p) робить те саме руками (обидва дають 270.0). А Продаж.сума() не отримує нічого — і функція падає на браку першого аргументу. Атрибут сума у класу є, тому AttributeError тут не буде.
Питання 4 / 8
У тілі класу написано історія = [], а метод робить self.історія.append(подія). Створили два продажі й записали по одній події в кожен. Що покаже len(перший.історія)?
Чому так. Список [] створюється один раз, коли Python читає тіло класу. append — не присвоєння, тому нового поля в примірнику не зʼявляється: пошук іде вгору до класу і дописує у спільний список. Інтерактив 6, перший режим: обидві довжини — 2, а p1.історія is p2.історія дає True. Ліки — створювати список у __init__, тобто на кожен виклик свій.
Питання 5 / 8
ставка_пдв = 0.2 оголошено в тілі класу. Виконали p1.ставка_пдв = 0.1, а потім Продаж.ставка_пдв = 0.05. Що покажуть p1.ставка_пдв і p2.ставка_пдв?
Чому так. Читання й запис несиметричні. Запис p1.ставка_пдв = 0.1 завжди кладе значення в сам примірник — у p1 зʼявляється власне поле, яке відтепер перекриває класове. У p2 свого поля немає, тому читання йде вгору до класу й бачить нове 0.05. Саме тому «зміна ставки для всіх» іноді працює не для всіх.
Питання 6 / 8
p = Продаж("кава", 3, 90.0); r = Продаж("кава", 3, 90.0). Чому p == r дає False?
Чому так. За замовчуванням клас успадковує порівняння за ідентичністю: == питає не про вміст, а про те, чи це один і той самий обʼєкт. p і r — два різні обʼєкти з різними id(), тому False. Навчити клас порівнюватись за вмістом можна, дописавши __eq__ (тема 27). Словники тут ні до чого: p.__dict__ == r.__dict__ якраз дало б True.
Питання 7 / 8
Клас без __repr__ друкується як <__main__.Продаж object at 0x7f2c4a13a840>. У якому місці це болить найдужче?
Чому так. Інтерактив 7, третій рядок: print([p, q]) друкує елементи через repr кожного — і без цього методу ти бачиш два шістнадцяткові числа замість даних. Ані порівняння, ані доступ до полів, ані запис у файл від __repr__ не залежать; це метод саме про те, як обʼєкт виглядає у виводі.
Питання 8 / 8
Коли клас у Python — зайвий?
Чому так. Клас виправданий поведінкою — правилами, перевірками, обчисленнями, що мають жити біля даних. Клас Калькулятор з одним методом порахувати(x) і без стану — це функція, яку змусили одягнути піджак; набір геттерів і сетерів у Python теж не потрібен, бо атрибут читають напряму. Кількість полів і походження даних тут ні до чого.