Домашнє завдання

Аргументи й область видимості

Три рівні. Кожен наступний спирається на попередній, але братися можна за будь-який. Пиши в окремому файлі 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 · Пастка своїми руками, потім ліки

Напиши дві версії функції зібрати_чек(товар, рядки=...), яка дописує назву товару в список рядків і повертає цей список:

Виклич кожну по три рази поспіль і доведи різницю 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 · Змінити проти повернути

Напиши дві функції з однаковим змістом і різною поведінкою:

Зроблено, якщо: виконуються

вихідний = ["хліб"]
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, перейменування своєї змінної) ти обереш у реальному коді й чому.


Підказки