Працюємо з тим самим каталогом бібліотеки, що й у лекції. Усе роби в окремому файлі
katalog.py або в новому зошиті — практику не редагуй, вона знадобиться для звірки.
рівень 1Рівень 1 — База
Збери клас Видання з повним набором методів і доведи assertами, що кожен із них
справді викликається.
- Напиши
Видання(назва, рік, сторінок)— три поля замість двох. - Додай
__repr__(відтворюваний:Видання('Кобзар', 1840, 114)) і__str__(людський:Кобзар (1840)). - Додай
__eq__і__hash__за всіма трьома полями — обовʼязково разом. - Додай
__len__, який повертає кількість сторінок. - Створи три видання, з яких два мають однаковий вміст, і поклади всі три в множину.
Зроблено, якщо:
assert repr(видання) == "Видання('Кобзар', 1840, 114)"проходить;assert eval(repr(видання)) == виданняпроходить — тобто__repr__справді відтворює обʼєкт, а__eq__це підтверджує;assert len(множина) == 2проходить — дублікат схлопнувся;assert len(видання) == 114іassert bool(видання) is Trueпроходять;- є
assert hash(к1) == hash(к2)для двох рівних видань.
рівень 2Рівень 2 — Плюс
Тепер контейнер і власний менеджер контексту.
- Напиши клас
Полиця, який тримає список видань і реалізує__len__,__getitem__,__iter__,__contains__і__repr__. - Додай
__add__:полиця1 + полиця2повертає нову полицю з усіма позиціями обох. Вихідні полиці не змінюються. - Додай
__lt__дляВидання(порядок за роком, потім за назвою) і методПолиця.за_роком(), який повертає нову полицю з відсортованими позиціями. - Напиши менеджер контексту
Інвентаризація, який на вході запамʼятовуєlen(полиця), а на виході друкує, скільки позицій додали чи прибрали за час блоку, і записує різницю в атрибутзміна.
Зроблено, якщо:
assert len(полиця1 + полиця2) == len(полиця1) + len(полиця2)проходить, і одразу після ньогоassert len(полиця1) == довжина_до— доказ, що+нічого не зіпсував;assert (полиця1 + полиця2) is not полиця1проходить;assert [в.рік for в in полиця.за_роком()] == sorted(в.рік for в in полиця)проходить;- після блоку
with Інвентаризація(полиця) as інв:, у якому додали дві позиції, проходитьassert інв.зміна == 2; - є другий блок
with, усередині якого кинуто виняток, і після нього проходитьassert інв2.зміна is not None— тобто__exit__відпрацював попри виняток.
рівень 3Рівень 3 — Виклик
Дослідження меж: що саме ламається, коли контракт порушено, і скільки це коштує.
- Зміряй ціну хешу. Зроби два класи з однаковим
__eq__: у першого__hash__рахується з кортежа полів, у другого —return 1(формально контракт не порушено: рівні обʼєкти дають рівний хеш!). Заведи по 5000 різних видань кожного класу, поклади в множину й зміряйtime.perf_counter()для побудови та для 5000 перевірокin. - Поясни цифри. Запиши в коментарі, чому другий варіант повільніший і на скільки разів, спираючись на розділ про хеш-таблицю з теми 8: що відбувається, коли всі ключі потрапляють в одну комірку.
- Зламай обʼєкт зсередини множини. Візьми клас із коректним
__hash__, поклади обʼєкт у множину, а потім зміни поле, від якого хеш залежить. Покажиassertами, що обʼєкт більше не знаходиться в множині — навіть за самим собою, — хочаlen(множина)і далі дорівнює одиниці. - Полагодь. Переробі клас так, щоб зламати його стало неможливо: поля
тільки читаються (
propertyбез сетера або__slots__плюс запис у__init__черезobject.__setattr__), а «зміна» повертає новий обʼєкт методомз_роком(новий_рік).
Зроблено, якщо:
- зафіксовані обидва заміри з числами, і повільніший варіант програє щонайменше
вдесятеро на перевірках
in; - у коментарі своїми словами пояснено, у що перетворюється хеш-таблиця, коли всі ключі дають одну комірку, і яка складність пошуку виходить замість очікуваної;
- проходить
assert обʼєкт not in множинапісля зміни поля, і поручassert len(множина) == 1— обидва одночасно; - на полагодженому класі проходить
assert видання.з_роком(1841) is not виданняі спробавидання.рік = 1841кидає виняток (спіймай йогоtry/exceptі надрукуй тип).
Підказки
- Рівень 1. Якщо
eval(repr(видання)) == виданняне проходить, дивись не на__eq__, а на__repr__: найчастіше в ньому забувають!rбіля рядкового поля, і назва виходить без лапок — такий вираз просто не є коректним Python. - Рівень 2.
__add__має повертатиПолиця(список1 + список2), а не дописувати вself. Якщоlen(полиця1)після додавання змінилась — ти десь написав.extend. - Рівень 2.
__exit__отримує три аргументи; щоб зрозуміти, чи був виняток, досить перевірити перший наis not None. Повертати з ньогоTrueне треба — інвентаризація не повинна ковтати чужі помилки. - Рівень 3. Хеш
return 1не порушує контракту, але знищує сенс хешування: усі ключі опиняються в одній комірці, і множина вироджується в те, від чого хеш-таблиця мала врятувати. - Рівень 3. Найпростіший спосіб зробити поле незмінним — оголосити його
через
@propertyі не писати сетер. Тоді присвоєння кинеAttributeErrorіз зрозумілим текстом.