Практика · Магічні методи¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Наскрізний приклад той самий, що в лекції, — каталог невеликої бібліотеки. Тут ми руками зробимо все, про що йшлося:
- переконаємось, що
len(x)— це справді викликtype(x).__len__(x), а не магія; - навчимо
Виданняпоказувати себе через__repr__і__str__; - додамо
__eq__— і зламаємо контракт із__hash__, щоб побачити дублікат у множині власними очима, а потім полагодимо; - спробуємо відсортувати каталог без
__lt__і з ним; - зробимо
Каталогтаким, що знає свою довжину, істинність, індекс і перебір; - напишемо
ЖурналВидач— власний менеджер контексту, який закриває справжній файл навіть тоді, коли всередині блоку кинуто виняток; - і наприкінці звіримо свій клас із вбудованими типами — переконаємось, що всередині списку й кортежа немає нічого, чого ми щойно не написали самі.
Нічого, крім стандартної бібліотеки, тут не потрібно.
1 · Протокол: len(x) — це виклик методу¶
Почнемо з того, що вже працює. Візьмемо звичайний список і покличемо його довжину трьома способами: коротким записом, методом напряму й методом, узятим із типу. Якщо це справді один і той самий виклик, усі три дадуть одне число.
спис = [10, 20, 30]
print("len(спис) =", len(спис))
print("спис.__len__() =", спис.__len__())
print("type(спис).__len__(спис) =", type(спис).__len__(спис))
# три записи мають бути буквально одним і тим самим викликом
assert len(спис) == спис.__len__() == type(спис).__len__(спис), "це не той самий виклик!"
print("✅ len() — це коротка форма запису type(x).__len__(x)")
len(спис) = 3 спис.__len__() = 3 type(спис).__len__(спис) = 3 ✅ len() — це коротка форма запису type(x).__len__(x)
2 · Клас без жодного магічного методу¶
Тепер наш Видання — поки що тільки з __init__. Створимо два обʼєкти
з однаковим вмістом і подивимось, як Python їх показує й порівнює.
class Видання:
def __init__(self, назва, рік):
self.назва = назва
self.рік = рік
к1 = Видання("Кобзар", 1840)
к2 = Видання("Кобзар", 1840) # той самий вміст, інший обʼєкт
print("repr(к1) =", repr(к1))
print("str(к1) =", str(к1))
print("к1 == к2 =", к1 == к2)
print("к1 is к2 =", к1 is к2)
# без __eq__ рівність означає те саме, що тотожність: «це той самий обʼєкт?»
assert (к1 == к2) is False, "без __eq__ два різні обʼєкти не мають бути рівні"
print("✅ за замовчуванням == працює як is")
repr(к1) = <__main__.Видання object at 0x7fc2c3489fd0> str(к1) = <__main__.Видання object at 0x7fc2c3489fd0> к1 == к2 = False к1 is к2 = False ✅ за замовчуванням == працює як is
3 · __repr__ і __str__¶
Додаємо два методи показу. Далі перевіримо чотири звичайні місця виводу й побачимо,
де спрацьовує який: консоль і список беруть __repr__, а print і f-рядок — __str__.
class Видання:
def __init__(self, назва, рік):
self.назва = назва
self.рік = рік
def __repr__(self):
# !r підставляє repr назви, тому вона виходить у лапках — видно, що це текст
return f"Видання({self.назва!r}, {self.рік})"
def __str__(self):
return f"{self.назва} ({self.рік})"
к1 = Видання("Кобзар", 1840)
print("repr(к1) →", repr(к1))
print("str(к1) →", str(к1))
print("f-рядок →", f"Позиція: {к1}")
print("усередині списку →", [к1])
assert repr(к1) == "Видання('Кобзар', 1840)", "__repr__ має бути схожий на виклик конструктора"
assert str(к1) == "Кобзар (1840)", "__str__ — рядок для людини"
print("✅ два методи — два адресати")
repr(к1) → Видання('Кобзар', 1840)
str(к1) → Кобзар (1840)
f-рядок → Позиція: Кобзар (1840)
усередині списку → [Видання('Кобзар', 1840)]
✅ два методи — два адресати
Найкорисніша властивість __repr__ — його відтворюваність: рядок можна скопіювати
назад у код і отримати такий самий обʼєкт. Перевіримо це буквально, через eval.
eval виконує рядок як вираз Python. У справжніх програмах його майже не використовують
(це дірка в безпеці, якщо рядок прийшов ззовні), але тут він робить рівно те, що треба, —
доводить, що наш __repr__ не збрехав.
відтворене = eval(repr(к1)) # той самий вираз, що ми надрукували
print("було: ", repr(к1))
print("відтворено:", repr(відтворене))
print("це різні обʼєкти:", відтворене is not к1)
assert repr(відтворене) == repr(к1), "repr має відтворювати обʼєкт"
print("✅ repr відтворюваний")
було: Видання('Кобзар', 1840)
відтворено: Видання('Кобзар', 1840)
це різні обʼєкти: True
✅ repr відтворюваний
4 · __eq__ — рівність за вмістом¶
Тепер навчимо Видання порівнюватись за назвою й роком. Порівнюємо кортеж із кортежем:
один рядок замість двох перевірок через and, і легко дописати третє поле.
NotImplemented у першій гілці — це ввічлива відмова «я не знаю, як порівняти себе
з цим». Отримавши її, Python спитає з іншого боку, і лише потім поверне False.
class ВиданняТількиРівність:
def __init__(self, назва, рік):
self.назва = назва
self.рік = рік
def __repr__(self):
return f"Видання({self.назва!r}, {self.рік})"
def __eq__(self, інший):
if not isinstance(інший, ВиданняТількиРівність):
return NotImplemented # хай спробує другий бік
return (self.назва, self.рік) == (інший.назва, інший.рік)
а = ВиданняТількиРівність("Кобзар", 1840)
б = ВиданняТількиРівність("Кобзар", 1840)
в = ВиданняТількиРівність("Місто", 1928)
print("а == б (той самий вміст) =", а == б)
print("а == в (інший вміст) =", а == в)
print("а == 'Кобзар' =", а == "Кобзар")
assert а == б, "однаковий вміст — однакові видання"
assert а != в, "різний вміст — різні видання"
print("✅ рівність тепер за вмістом")
а == б (той самий вміст) = True а == в (інший вміст) = False а == 'Кобзар' = False ✅ рівність тепер за вмістом
І одразу наслідок, про який ніхто не попереджає¶
Щойно ми визначили __eq__, Python автоматично поставив нашому класу
__hash__ = None. Це запобіжник: клас із власною рівністю більше не можна класти
в множину чи брати як ключ словника, доки ти не скажеш, як рахувати хеш.
Наступна клітинка навмисно падає — подивись на текст помилки, він знайомий із теми 8 про словники.
print("__hash__ нашого класу:", ВиданняТількиРівність.__hash__)
множина = {а, б} # ← ось тут і впаде
print("сюди ми не дійдемо:", множина)
__hash__ нашого класу: None
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[6], line 3 1 print("__hash__ нашого класу:", ВиданняТількиРівність.__hash__) ----> 3 множина = {а, б} # ← ось тут і впаде 4 print("сюди ми не дійдемо:", множина) TypeError: unhashable type: 'ВиданняТількиРівність'
5 · Порушений контракт — головна вправа теми¶
Найпоширеніший «ремонт» цієї помилки — повернути класу хеш від object:
__hash__ = object.__hash__. Помилка справді зникає. Але хеш від object
рахується з адреси обʼєкта, а рівність ми рахуємо з вмісту.
Виходить, що два рівні обʼєкти мають різні хеші, тобто лягають у різні комірки хеш-таблиці й ніколи не зустрічаються. Перевіримо, чим це закінчується.
class ВиданняЗламане:
def __init__(self, назва, рік):
self.назва = назва
self.рік = рік
def __repr__(self):
return f"Видання({self.назва!r}, {self.рік})"
def __eq__(self, інший):
if not isinstance(інший, ВиданняЗламане):
return NotImplemented
return (self.назва, self.рік) == (інший.назва, інший.рік)
# «полагодили» TypeError — і цим зламали контракт: хеш рахується з адреси
__hash__ = object.__hash__
з1 = ВиданняЗламане("Кобзар", 1840)
з2 = ВиданняЗламане("Кобзар", 1840)
print("з1 == з2 :", з1 == з2)
print("hash однакові :", hash(з1) == hash(з2))
print("множина :", {з1, з2})
print("розмір множини:", len({з1, з2}))
# ось те, заради чого вся вправа: рівні обʼєкти, а в множині їх двоє
assert з1 == з2, "обʼєкти рівні"
assert len({з1, з2}) == 2, "порушений контракт має давати дублікат у множині"
print("✅ контракт порушено: два РІВНІ обʼєкти лежать у множині окремо")
з1 == з2 : True
hash однакові : False
множина : {Видання('Кобзар', 1840), Видання('Кобзар', 1840)}
розмір множини: 2
✅ контракт порушено: два РІВНІ обʼєкти лежать у множині окремо
Дублікат — половина біди. Друга половина в тому, що словник не знаходить ключа, хоча такий ключ у ньому є і він дорівнює тому, який ми шукаємо.
ціни = {з1: "240,00 грн"}
print("ключ з1 у словнику:", з1 in ціни)
print("ключ з2 у словнику:", з2 in ціни) # хоча з2 == з1
print("а рівні вони? :", з1 == з2)
assert з1 in ціни, "той самий обʼєкт знаходиться завжди"
assert з2 not in ціни, "рівний обʼєкт з іншим хешем словник не знайде"
print("⚠️ словник не знайшов ключа, який дорівнює наявному — і жодної помилки")
ключ з1 у словнику: True ключ з2 у словнику: False а рівні вони? : True ⚠️ словник не знайшов ключа, який дорівнює наявному — і жодної помилки
6 · Контракт виконано¶
Лікується це одним рядком: хеш рахуємо з тих самих полів, за якими порівнюємо. Кортеж уміє хешуватись сам і робить це узгоджено з власною рівністю, тож нічого вигадувати не треба.
class Видання:
def __init__(self, назва, рік):
self.назва = назва
self.рік = рік
def __repr__(self):
return f"Видання({self.назва!r}, {self.рік})"
def __str__(self):
return f"{self.назва} ({self.рік})"
def __eq__(self, інший):
if not isinstance(інший, Видання):
return NotImplemented
return (self.назва, self.рік) == (інший.назва, інший.рік)
def __hash__(self):
# ті самі поля, що й у __eq__ — і контракт виконується сам собою
return hash((self.назва, self.рік))
к1 = Видання("Кобзар", 1840)
к2 = Видання("Кобзар", 1840)
ціни = {к1: "240,00 грн"}
print("к1 == к2 :", к1 == к2)
print("хеші однакові :", hash(к1) == hash(к2))
print("множина :", {к1, к2})
print("ціни[к2] :", ціни[к2])
assert hash(к1) == hash(к2), "рівні обʼєкти зобовʼязані мати рівні хеші"
assert len({к1, к2}) == 1, "дубліката бути не має"
assert ціни[к2] == "240,00 грн", "словник має знаходити ключ за вмістом"
print("✅ контракт виконано: множина без дублікатів, словник шукає за вмістом")
к1 == к2 : True
хеші однакові : True
множина : {Видання('Кобзар', 1840)}
ціни[к2] : 240,00 грн
✅ контракт виконано: множина без дублікатів, словник шукає за вмістом
7 · Порядок: без __lt__ і з ним¶
Заведемо чотири видання в довільному порядку й спробуємо їх відсортувати. Наступна клітинка навмисно падає: у класі немає жодного методу порівняння, і Python чесно каже, якої саме операції бракує.
позиції = [
Видання("Тигролови", 1944),
Видання("Кобзар", 1840),
Видання("Місто", 1928),
Видання("Лісова пісня", 1911),
]
print("позицій у каталозі:", len(позиції))
print(sorted(позиції)) # ← ось тут і впаде
позицій у каталозі: 4
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[10], line 9 1 позиції = [ 2 Видання("Тигролови", 1944), 3 Видання("Кобзар", 1840), 4 Видання("Місто", 1928), 5 Видання("Лісова пісня", 1911), 6 ] 8 print("позицій у каталозі:", len(позиції)) ----> 9 print(sorted(позиції)) # ← ось тут і впаде TypeError: '<' not supported between instances of 'Видання' and 'Видання'
Додаємо __lt__. Одного цього методу вистачає одразу для чотирьох речей:
<, sorted, min і max. А > Python виведе дзеркально — це просто b < a.
class Видання(Видання): # той самий клас, лише дописуємо порядок
def __lt__(self, інший):
if not isinstance(інший, Видання):
return NotImplemented
# рік головний, назва — щоб порядок був однозначний у межах одного року
return (self.рік, self.назва) < (інший.рік, інший.назва)
позиції = [
Видання("Тигролови", 1944),
Видання("Кобзар", 1840),
Видання("Місто", 1928),
Видання("Лісова пісня", 1911),
]
за_роком = sorted(позиції)
for в in за_роком:
print(f" {в.рік} {в.назва}")
print("найстаріше:", min(позиції))
print("найновіше :", max(позиції))
print("Тигролови > Кобзар:", позиції[0] > позиції[1])
роки = [в.рік for в in за_роком]
assert роки == sorted(роки), "після sorted роки мають іти за зростанням"
assert min(позиції).назва == "Кобзар", "найстаріше видання — Кобзар"
assert max(позиції).назва == "Тигролови", "найновіше — Тигролови"
print("✅ один __lt__ дав сортування, min, max і навіть >")
1840 Кобзар 1911 Лісова пісня 1928 Місто 1944 Тигролови найстаріше: Кобзар (1840) найновіше : Тигролови (1944) Тигролови > Кобзар: True ✅ один __lt__ дав сортування, min, max і навіть >
А ось <= з __lt__ не виводиться: «не менше» не є запереченням «менше» в загальному
випадку, і Python не вгадує. Клітинка навмисно падає.
print("порівнюємо:", позиції[1], "і", позиції[0])
print(позиції[1] <= позиції[0]) # ← ось тут і впаде
порівнюємо: Кобзар (1840) і Тигролови (1944)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[12], line 2 1 print("порівнюємо:", позиції[1], "і", позиції[0]) ----> 2 print(позиції[1] <= позиції[0]) # ← ось тут і впаде TypeError: '<=' not supported between instances of 'Видання' and 'Видання'
Якщо потрібні всі чотири порівняння, їх дописує декоратор total_ordering
зі стандартної бібліотеки. Йому потрібні __eq__ і один метод порядку.
from functools import total_ordering
@total_ordering
class ВиданняПовне(Видання):
pass # __eq__ і __lt__ уже успадковані, решту допише декоратор
а = ВиданняПовне("Кобзар", 1840)
б = ВиданняПовне("Місто", 1928)
print("а < б :", а < б)
print("а <= б :", а <= б)
print("а >= б :", а >= б)
print("а > б :", а > б)
assert (а <= б) is True and (а >= б) is False, "total_ordering має дописати обидва"
print("✅ @total_ordering дописав __le__, __ge__ і __gt__")
а < б : True а <= б : True а >= б : False а > б : False ✅ @total_ordering дописав __le__, __ge__ і __gt__
8 · Каталог: довжина, істинність, індекс, перебір¶
Тепер контейнер. Почнемо з __len__ — і одразу побачимо, що він дає дві речі:
довжину й правильну істинність. Порожній каталог має бути хибним, як порожній список.
class Каталог:
def __init__(self, позиції):
self.позиції = list(позиції)
def __repr__(self):
return f"Каталог({self.позиції!r})"
def __str__(self):
return f"каталог, позицій: {len(self.позиції)}"
def __len__(self):
return len(self.позиції)
каталог = Каталог(позиції)
порожній = Каталог([])
print("len(каталог) :", len(каталог))
print("bool(каталог) :", bool(каталог))
print("len(порожній) :", len(порожній))
print("bool(порожній) :", bool(порожній))
assert len(каталог) == 4, "у каталозі чотири позиції"
assert bool(порожній) is False, "порожній контейнер має бути хибним"
print("✅ один __len__ дав і довжину, і істинність")
len(каталог) : 4 bool(каталог) : True len(порожній) : 0 bool(порожній) : False ✅ один __len__ дав і довжину, і істинність
Тепер перебір. Спершу перевіримо, що зараз каталог перебрати не можна — клітинка навмисно падає.
print("у каталозі позицій:", len(каталог))
for видання in каталог: # ← ось тут і впаде
print(видання)
у каталозі позицій: 4
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[15], line 2 1 print("у каталозі позицій:", len(каталог)) ----> 2 for видання in каталог: # ← ось тут і впаде 3 print(видання) TypeError: 'Каталог' object is not iterable
Дописуємо три методи контейнера. __getitem__ передає індекс далі, у внутрішній
список, — тому IndexError на виході за межі кине сам список, і нам не треба про це
дбати. __iter__ віддає готовий ітератор списку, а __contains__ робить in
явним і швидшим.
class Каталог(Каталог): # доповнюємо той самий клас
def __getitem__(self, індекс):
return self.позиції[індекс] # список сам кине IndexError, коли треба
def __iter__(self):
return iter(self.позиції)
def __contains__(self, видання):
return видання in self.позиції
каталог = Каталог(позиції)
print("каталог[0] :", каталог[0])
print("каталог[-1] :", каталог[-1])
print("зріз каталог[:2]:", каталог[:2])
print("перебір циклом :")
for видання in каталог:
print(" ", видання)
print('Видання("Кобзар", 1840) in каталог:', Видання("Кобзар", 1840) in каталог)
assert list(каталог) == позиції, "перебір має віддавати ті самі позиції"
assert Видання("Кобзар", 1840) in каталог, "in шукає за вмістом, бо є __eq__"
assert Видання("Аенеїда", 1798) not in каталог, "чужого видання в каталозі немає"
print("✅ каталог поводиться як контейнер")
каталог[0] : Тигролови (1944)
каталог[-1] : Лісова пісня (1911)
зріз каталог[:2]: [Видання('Тигролови', 1944), Видання('Кобзар', 1840)]
перебір циклом :
Тигролови (1944)
Кобзар (1840)
Місто (1928)
Лісова пісня (1911)
Видання("Кобзар", 1840) in каталог: True
✅ каталог поводиться як контейнер
9 · Арифметика там, де вона доречна: Гроші¶
Бібліотека купує книжки, а гроші не можна тримати в float — з теми 4 ми знаємо,
чому 0.1 + 0.2 не дорівнює 0.3. Тримаємо суму в копійках цілим числом.
Зверни увагу: __add__ повертає новий обʼєкт, а не змінює себе, — так поводяться
всі числа в Python. А __radd__ потрібен, щоб працював sum(): він починає з нуля,
і перше додавання виглядає як 0 + Гроші(...).
class Гроші:
def __init__(self, копійки):
self.копійки = копійки
def __repr__(self):
return f"Гроші({self.копійки})"
def __str__(self):
return f"{self.копійки // 100},{self.копійки % 100:02d} грн"
def __eq__(self, інший):
if not isinstance(інший, Гроші):
return NotImplemented
return self.копійки == інший.копійки
def __hash__(self):
return hash(self.копійки)
def __add__(self, інший):
if not isinstance(інший, Гроші):
return NotImplemented
return Гроші(self.копійки + інший.копійки)
def __mul__(self, множник):
if not isinstance(множник, int):
return NotImplemented # на гроші гроші не множать
return Гроші(self.копійки * множник)
def __radd__(self, інший):
# sum() починає з нуля: перший крок — це 0 + Гроші(...)
if інший == 0:
return self
return NotImplemented
закупівля = [Гроші(24000), Гроші(15050), Гроші(9990)]
print("перша + друга :", закупівля[0] + закупівля[1])
print("перша × 3 :", закупівля[0] * 3)
print("sum(закупівля):", sum(закупівля))
assert закупівля[0] + закупівля[1] == Гроші(39050), "додавання рахує копійки"
assert закупівля[0] * 3 == Гроші(72000), "множення на ціле число"
assert sum(закупівля) == Гроші(49040), "sum працює завдяки __radd__"
print("✅ арифметика працює й не втрачає копійок")
перша + друга : 390,50 грн перша × 3 : 720,00 грн sum(закупівля): 490,40 грн ✅ арифметика працює й не втрачає копійок
10 · Власний менеджер контексту¶
Обіцянка з теми 20: обʼєкт, який уміє стояти після with, — це два методи.
__enter__ віддає те, що потрапить у as, а __exit__ викликається завжди:
і коли блок дійшов до кінця, і коли всередині кинуто виняток.
Писатимемо в тимчасову теку, щоб не смітити поруч із зошитом.
import pathlib
import tempfile
тека = pathlib.Path(tempfile.mkdtemp())
шлях_журналу = тека / "vydachi.txt"
class ЖурналВидач:
def __init__(self, шлях):
self.шлях = шлях
self.файл = None
self.записано = 0
def __enter__(self):
self.файл = open(self.шлях, "a", encoding="utf-8")
return self # саме це значення отримає імʼя після as
def записати(self, видання):
# поля беремо явно — так видно, на чому саме спіткнеться чужий обʼєкт
self.файл.write(f"{видання.назва} ({видання.рік})\n")
self.записано += 1
def __exit__(self, тип, значення, слід):
self.файл.close()
# запамʼятовуємо, з чим нас покликали, — щоб потім це перевірити
self.вихід_з_винятком = тип is not None
return False # виняток летить далі: прибираємо, а не лікуємо
with ЖурналВидач(шлях_журналу) as журнал:
журнал.записати(Видання("Кобзар", 1840))
журнал.записати(Видання("Лісова пісня", 1911))
print("файл закрито :", журнал.файл.closed)
print("вийшли з винятком :", журнал.вихід_з_винятком)
print("вміст файла :", шлях_журналу.read_text(encoding="utf-8").split("\n")[:-1])
assert журнал.файл.closed, "__exit__ мусив закрити файл"
assert журнал.вихід_з_винятком is False, "блок завершився без винятку"
assert журнал.записано == 2, "два записи"
print("✅ звичайний вихід із блоку")
файл закрито : True вийшли з винятком : False вміст файла : ['Кобзар (1840)', 'Лісова пісня (1911)'] ✅ звичайний вихід із блоку
А тепер найцікавіше: кидаємо виняток посеред блоку. Файл усе одно має бути
закритий, а перший запис — долетіти на диск. Виняток ловимо звичайним try/except
навколо with, як ми домовлялись у темі 18.
шлях_другий = тека / "vydachi-2.txt"
спійманий = None
try:
with ЖурналВидач(шлях_другий) as журнал2:
журнал2.записати(Видання("Місто", 1928))
журнал2.записати(None) # None не має атрибута назва — впаде
журнал2.записати(Видання("Тигролови", 1944)) # сюди вже не дійдемо
except AttributeError as помилка:
спійманий = помилка
print("спіймали виняток :", type(спійманий).__name__)
print("файл закрито :", журнал2.файл.closed)
print("вийшли з винятком :", журнал2.вихід_з_винятком)
print("рядків на диску :", len(шлях_другий.read_text(encoding="utf-8").splitlines()))
assert спійманий is not None, "виняток мав вилетіти назовні: __exit__ повернув False"
assert журнал2.файл.closed, "файл мусить бути закритий навіть після винятку"
assert журнал2.вихід_з_винятком is True, "__exit__ отримав тип винятку, а не None"
assert len(шлях_другий.read_text(encoding="utf-8").splitlines()) == 1, "один запис долетів"
print("✅ __exit__ спрацював попри виняток — це і є гарантія with")
спіймали виняток : AttributeError файл закрито : True вийшли з винятком : True рядків на диску : 1 ✅ __exit__ спрацював попри виняток — це і є гарантія with
Виняток AttributeError виник тому, що None не має атрибута назва, — його
шукає f-рядок усередині записати. Головне тут інше: менеджер відпрацював обидва
рази однаково, і саме тому файловий дескриптор не тече.
11 · __call__: обʼєкт, який можна викликати¶
Функція нічого не памʼятає між викликами, а обʼєкт памʼятає. Зробимо «функцію з налаштуванням і лічильником»: поріг задається один раз при створенні, а статистика накопичується сама.
class ВиданіПісля:
def __init__(self, рік):
self.рік = рік
self.викликів = 0
def __call__(self, видання):
self.викликів += 1
return видання.рік > self.рік
новинки = ВиданіПісля(1900)
print("Кобзар новинка? :", новинки(Видання("Кобзар", 1840)))
print("після фільтра :", [str(в) for в in filter(новинки, каталог)])
print("разів покликали :", новинки.викликів)
assert callable(новинки), "обʼєкт із __call__ вважається викликуваним"
assert новинки.викликів == 5, "один прямий виклик плюс чотири від filter"
print("✅ обʼєкт спрацював там, де чекали функцію, і порахував себе сам")
Кобзар новинка? : False після фільтра : ['Тигролови (1944)', 'Місто (1928)', 'Лісова пісня (1911)'] разів покликали : 5 ✅ обʼєкт спрацював там, де чекали функцію, і порахував себе сам
12 · Звіряємось із вбудованими типами¶
Обовʼязкова перевірка практики: наш Каталог має поводитись точно так само,
як звичайний список, а наше Видання — так само, як кортеж (назва, рік).
Якщо збігається все, значить, усередині вбудованих типів немає нічого, крім тих самих
магічних методів, які ми щойно написали руками.
звичайний_список = list(позиції)
print(f"{'перевірка':<26} {'наш Каталог':<16} список")
пари = [
("len", len(каталог), len(звичайний_список)),
("bool", bool(каталог), bool(звичайний_список)),
("перший елемент", каталог[0], звичайний_список[0]),
("останній елемент", каталог[-1], звичайний_список[-1]),
("in (Кобзар)", Видання("Кобзар", 1840) in каталог,
Видання("Кобзар", 1840) in звичайний_список),
("довжина перебору", len(list(каталог)), len(list(звичайний_список))),
]
for назва, наше, вбудоване in пари:
print(f"{назва:<26} {str(наше):<16} {вбудоване}")
assert наше == вбудоване, f"розійшлись на перевірці «{назва}»!"
print("✅ наш контейнер не відрізняється від списку в жодній із шести перевірок")
перевірка наш Каталог список len 4 4 bool True True перший елемент Тигролови (1944) Тигролови (1944) останній елемент Лісова пісня (1911) Лісова пісня (1911) in (Кобзар) True True довжина перебору 4 4 ✅ наш контейнер не відрізняється від списку в жодній із шести перевірок
# те саме для елемента: рівність, хеш і порядок мають збігатися з кортежем
наші = [Видання("Кобзар", 1840), Видання("Кобзар", 1840), Видання("Місто", 1928)]
кортежі = [(1840, "Кобзар"), (1840, "Кобзар"), (1928, "Місто")]
наші_ключі = sorted((в.рік, в.назва) for в in set(наші))
кортежі_ключі = sorted(set(кортежі))
print("унікальних видань:", len(set(наші)))
print("унікальних кортежів:", len(set(кортежі)))
print("наші після dedup :", наші_ключі)
print("кортежі після dedup:", кортежі_ключі)
assert len(set(наші)) == len(set(кортежі)), "множина має схлопнути дублікат так само"
assert наші_ключі == кортежі_ключі, "порядок сортування має збігатися з кортежами"
print("✅ наш __eq__ + __hash__ + __lt__ поводяться як у кортежа")
унікальних видань: 2 унікальних кортежів: 2 наші після dedup : [(1840, 'Кобзар'), (1928, 'Місто')] кортежі після dedup: [(1840, 'Кобзар'), (1928, 'Місто')] ✅ наш __eq__ + __hash__ + __lt__ поводяться як у кортежа
13 · Завдання¶
Роби в цьому ж зошиті, нижче. Розгорнутіші варіанти цих завдань — у homework.md.
🟢 Рівень 1. Додай класу Видання метод __len__, який повертає довжину назви,
і перевір assertом, що len(Видання("Кобзар", 1840)) == 6. Потім подумай і напиши
в коментарі одним реченням, чому це погана ідея (підказка: що тепер означає
if видання: для видання з порожньою назвою?).
🟡 Рівень 2. Напиши клас Полиця, який тримає видання й уміє +:
полиця1 + полиця2 має повертати нову полицю з усіма позиціями обох, не змінюючи
жодну з вихідних. Доведи assertами три речі: результат містить усі позиції,
довжини вихідних полиць не змінились, і полиця1 is not результат.
🔴 Рівень 3. Напиши менеджер контексту ТихийЖурнал, який проковтує
AttributeError (повертає True з __exit__), а всі інші винятки пропускає далі.
Доведи assertами обидві поведінки: після AttributeError код після блоку with
виконується, а після ZeroDivisionError — ні. І запиши в коментарі, чому такий
менеджер небезпечний.
# ↓ тут твій код
print("готово")
готово