Три рівні. Кожен наступний спирається на попередній, але братися можна за будь-який.
Пиши в окремому файлі homework_15.py або в новому зошиті — головне, щоб код
запускався й друкував результат.
З бібліотек дозволено: inspect, math, copy. Більше нічого не треба.
Класів (тема 25) і декораторів (тема 31) ми ще не проходили — усі завдання
розвʼязуються звичайними функціями, і саме так їх і треба розвʼязувати.
Наскрізні дані ті самі, що в лекції:
ЦІНИ = {"хліб": 28.5, "молоко": 32.0, "яблука": 19.9, "мед": 145.0, "сіль": 12.0}
рівень 1Рівень 1 — База
1.1 · Своя функція з трьома видами аргументів
Напиши функцію знижка_за_обсяг(товар, кількість=1, поріг=5, відсоток=0.1), яка рахує
вартість позиції й дає знижку лише тоді, коли кількість не менша за поріг.
Виклич її чотирма різними способами: тільки позиційно, змішано (позиційно + іменовано), тільки іменовано в переставленому порядку і через розпакування словника.
Зроблено, якщо: усі чотири виклики надруковані з результатом і виконуються
assert знижка_за_обсяг("мед") == 145.0
assert знижка_за_обсяг("мед", 5) == round(145.0 * 5 * 0.9, 2)
assert знижка_за_обсяг("мед", 4) == round(145.0 * 4, 2), "нижче порогу знижки немає"
assert знижка_за_обсяг("мед", кількість=5, відсоток=0.2) == round(145.0 * 5 * 0.8, 2)
assert знижка_за_обсяг(**{"товар": "мед", "кількість": 5}) == знижка_за_обсяг("мед", 5)
1.2 · Три помилки виклику з поясненням
Не запускаючи код, випиши на папірці, що станеться в кожному з пʼяти рядків, і лише потім перевір себе:
знижка_за_обсяг()
знижка_за_обсяг("мед", 5, кількість=2)
знижка_за_обсяг(кількість=5, "мед")
знижка_за_обсяг("мед", вага=2)
знижка_за_обсяг("мед", 5, 3, 0.1, 7)
Зроблено, якщо: для кожного рядка ти надрукував тип помилки й текст повідомлення
(лови через try/except, а третій рядок доведеться закоментувати — поясни письмово
чому), і для кожного одним реченням пояснив, що саме не зійшлося.
1.3 · Заглянути в __defaults__
Надрукуй знижка_за_обсяг.__defaults__ і поясни письмово, чому там три значення,
а не чотири.
Зроблено, якщо: виконується
assert знижка_за_обсяг.__defaults__ == (1, 5, 0.1)
assert len(знижка_за_обсяг.__defaults__) == len(інспекція_параметрів) - 1
де інспекція_параметрів — список імен параметрів, який ти отримав через
inspect.signature(знижка_за_обсяг).parameters.
рівень 2Рівень 2 — Плюс
2.1 · Пастка своїми руками, потім ліки
Напиши дві версії функції зібрати_чек(товар, рядки=...), яка дописує назву товару
в список рядків і повертає цей список:
зібрати_чек_пастка— зі значенням за замовчуванням[];зібрати_чек— зі значеннямNoneі створенням списку в тілі.
Виклич кожну по три рази поспіль і доведи різницю assertами.
Зроблено, якщо: виконуються всі перевірки
п1 = зібрати_чек_пастка("хліб")
п2 = зібрати_чек_пастка("мед")
п3 = зібрати_чек_пастка("сіль")
assert len(п3) == 3, "пастка мала накопичити всі три виклики"
assert п1 is п2 is п3, "це має бути один і той самий обʼєкт"
assert зібрати_чек_пастка.__defaults__[0] is п1
н1 = зібрати_чек("хліб")
н2 = зібрати_чек("мед")
assert len(н1) == len(н2) == 1, "кожен виклик мав отримати свій список"
assert н1 is not н2
assert зібрати_чек.__defaults__ == (None,)
і ти письмово відповів: у який момент створюється список у першій версії і в який — у другій.
2.2 · Змінити проти повернути
Напиши дві функції з однаковим змістом і різною поведінкою:
додати_на_місці(кошик, товар)— змінює переданий список і повертаєNone;з_доданим(кошик, товар)— не чіпає вхід і повертає новий список.
Зроблено, якщо: виконуються
вихідний = ["хліб"]
id_до = id(вихідний)
assert додати_на_місці(вихідний, "мед") is None, "функція, що змінює, нічого не повертає"
assert вихідний == ["хліб", "мед"]
assert id(вихідний) == id_до, "обʼєкт мав лишитись тим самим"
інший = ["хліб"]
новий = з_доданим(інший, "мед")
assert інший == ["хліб"], "вхід мав лишитись недоторканим"
assert новий == ["хліб", "мед"] and новий is not інший
і ти назвав пару функцій зі стандартної бібліотеки, влаштовану за тим самим принципом.
2.3 · *args і **kwargs на практиці
Напиши функцію підсумок(*позиції, знижка=0.0, **службові), яка рахує суму цін
переданих товарів, застосовує знижку й ігнорує будь-які службові аргументи,
але друкує, які саме отримала.
Зроблено, якщо: виконуються
assert підсумок() == 0.0, "порожній кортеж — нульова сума"
assert підсумок("хліб", "сіль") == round(28.5 + 12.0, 2)
assert підсумок("хліб", "сіль", знижка=0.5) == round((28.5 + 12.0) * 0.5, 2)
assert підсумок("хліб", коментар="тест", джерело="каса") == 28.5
і ти письмово пояснив, чому знижка тут не потрапляє у **службові.
2.4 · Усі чотири рівні LEGB на одному імені
Напиши код, у якому імʼя ставка існує на рівнях L, E і G одночасно, а вкладена
функція повертає те, що бачить. Потім, прибираючи оголошення по одному, покажи,
як відповідь змінюється.
Зроблено, якщо: твій код друкує три різні значення в трьох різних станах і виконується
assert знайдене == ["локальне", "обгортка", "глобальне"]
де знайдене — список того, що повернула вкладена функція в кожному зі станів.
Плюс окремо покажи NameError для імені, якого немає ніде, і поясни, чому для
імені sum така помилка неможлива.
рівень 3Рівень 3 — Виклик
3.1 · Свій розподіл аргументів проти бібліотечного
Напиши функцію розкласти(параметри, замовчування, аргументи, ключові), яка сама
розкладає аргументи по параметрах і повертає словник «імʼя параметра → значення»:
параметри— список імен у порядку оголошення;замовчування— словник «імʼя → типове значення» (може бути неповним);аргументи— кортеж позиційних;ключові— словник іменованих.
Функція має піднімати TypeError у трьох випадках: параметр отримав два значення,
обовʼязковий параметр не отримав нічого, переданий невідомий ключ.
Зроблено, якщо: твій результат збігається з inspect.signature(...).bind(...)
щонайменше на пʼятьох викликах, включно з помилковими:
import inspect
def оформити(товар, кількість=1, знижка=0.0):
return товар, кількість, знижка
ПАРАМЕТРИ = ["товар", "кількість", "знижка"]
ЗАМОВЧУВАННЯ = {"кількість": 1, "знижка": 0.0}
підпис = inspect.signature(оформити)
перевірки = [
(("мед",), {}),
(("мед", 3), {}),
(("мед",), {"знижка": 0.1}),
((), {"кількість": 3, "товар": "мед"}),
]
for позиційні, іменовані in перевірки:
еталон = підпис.bind(*позиційні, **іменовані)
еталон.apply_defaults()
assert розкласти(ПАРАМЕТРИ, ЗАМОВЧУВАННЯ, позиційні, іменовані) == dict(еталон.arguments)
# і три помилкові — обидва способи мають дати TypeError
for позиційні, іменовані in [(("мед", 3), {"кількість": 2}), ((), {"кількість": 3}),
(("мед",), {"вага": 2})]:
try:
підпис.bind(*позиційні, **іменовані)
бібліотека_впала = False
except TypeError:
бібліотека_впала = True
try:
розкласти(ПАРАМЕТРИ, ЗАМОВЧУВАННЯ, позиційні, іменовані)
ми_впали = False
except TypeError:
ми_впали = True
assert бібліотека_впала == ми_впали == True
3.2 · Лічильник без жодного global
Напиши функцію зробити_лічильник(старт=0, крок=1), яка повертає функцію. Кожен
виклик поверненої функції збільшує внутрішній лічильник і повертає нове значення.
Слова global у розвʼязку бути не має.
Зроблено, якщо: виконуються
перший = зробити_лічильник()
другий = зробити_лічильник(старт=100, крок=10)
assert [перший(), перший(), перший()] == [1, 2, 3]
assert [другий(), другий()] == [110, 120]
assert перший() == 4, "лічильники не мають заважати один одному"
assert перший.__code__.co_freevars, "стан має жити в замиканні, а не в глобальній памʼяті"
Останній assert і є перевіркою на відсутність global: якби лічильник лежав на рівні
модуля, захоплених імен у поверненої функції не було б узагалі.
І окремо надрукуй перший.__code__.co_freevars та поясни одним реченням, що це за імена.
3.3 · Затінення, яке ламає чужий код
Напиши функцію безпечна_довжина(послідовність), яка повертає len(послідовність).
Потім навмисно затінь на рівні модуля імʼя len числом і покажи, що функція
перестала працювати, хоча її текст не змінювався. Далі відновіть доступ трьома
різними способами й порівняй їх.
Зроблено, якщо: твій код друкує повідомлення помилки й виконуються
assert безпечна_довжина([1, 2, 3]) == 3 # до затінення
len = 0
try:
безпечна_довжина([1, 2, 3])
зламалось = False
except TypeError:
зламалось = True
assert зламалось, "затінене імʼя мало зламати виклик"
del len
assert безпечна_довжина([1, 2, 3]) == 3 # після відновлення
плюс письмово: (1) чому помилка виникла всередині безпечна_довжина, хоча затінили ми
імʼя зовні; (2) який зі способів відновлення (del, звернення через модуль builtins,
перейменування своєї змінної) ти обереш у реальному коді й чому.
Підказки
- Пастку найпростіше довести через
is, а не через==. Два різні списки з однаковим вмістом рівні між собою, тому==тут нічого не доведе. Питання «це той самий обʼєкт?» — цеisабо порівнянняid(). __defaults__містить лише хвіст. Значення там ідуть у тому самому порядку, що й параметри, але починаючи з першого параметра із замовчуванням. Тому в 1.3 їх три, а не чотири.- У 2.3 знижка оголошена після зірочки. Усе, що стоїть у
defправіше за*щось, Python шукає за іменем серед звичайних параметрів і лише потім скидає решту в**службові. - У 3.1 починай з позиційних. Розклади їх по
параметри[0], параметри[1], …, запамʼятовуючи, які імена вже зайняті. Потім проходь іменовані: якщо імʼя вже зайняте — це «два значення»; якщо такого імені немає впараметри— це «невідомий ключ». І лише наприкінці підставляй замовчування й перевіряй, чи не лишилось порожніх. - У 3.2 тобі потрібен
nonlocal. Без нього присвоєння всередині вкладеної функції створить нове локальне імʼя, і лічильник щоразу починатиме спочатку — точніше, впаде зUnboundLocalErrorна першому ж кроці. - У 3.3 не поспішай із
del. Спершу подумай, у якому просторі імен опинилось твоєlen = 0і чому пошук усередині функції доходить до нього раніше, ніж до вбудованих.