# Домашнє завдання · Аргументи й область видимості

> Лекція: [lecture.html](lecture.html) · Практика: [practice.ipynb](practice.ipynb) · Тест: [quiz.html](quiz.html)

Три рівні. Кожен наступний спирається на попередній, але братися можна за будь-який.
Пиши в окремому файлі `homework_15.py` або в новому зошиті — головне, щоб код
запускався й друкував результат.

З бібліотек дозволено: `inspect`, `math`, `copy`. Більше нічого не треба.
Класів (тема 25) і декораторів (тема 31) ми ще не проходили — усі завдання
розвʼязуються звичайними функціями, і саме так їх і треба розвʼязувати.

Наскрізні дані ті самі, що в лекції:

```python
ЦІНИ = {"хліб": 28.5, "молоко": 32.0, "яблука": 19.9, "мед": 145.0, "сіль": 12.0}
```

---

## 🟢 Рівень 1 — База

### 1.1 · Своя функція з трьома видами аргументів

Напиши функцію `знижка_за_обсяг(товар, кількість=1, поріг=5, відсоток=0.1)`, яка рахує
вартість позиції й дає знижку **лише тоді**, коли кількість не менша за поріг.

Виклич її **чотирма** різними способами: тільки позиційно, змішано (позиційно +
іменовано), тільки іменовано в переставленому порядку і через розпакування словника.

**Зроблено, якщо:** усі чотири виклики надруковані з результатом і виконуються

```python
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 · Три помилки виклику з поясненням

Не запускаючи код, випиши на папірці, що станеться в кожному з пʼяти рядків, і лише
потім перевір себе:

```python
знижка_за_обсяг()
знижка_за_обсяг("мед", 5, кількість=2)
знижка_за_обсяг(кількість=5, "мед")
знижка_за_обсяг("мед", вага=2)
знижка_за_обсяг("мед", 5, 3, 0.1, 7)
```

**Зроблено, якщо:** для кожного рядка ти надрукував тип помилки й текст повідомлення
(лови через `try/except`, а третій рядок доведеться закоментувати — поясни письмово
чому), і для кожного одним реченням пояснив, що саме не зійшлося.

### 1.3 · Заглянути в `__defaults__`

Надрукуй `знижка_за_обсяг.__defaults__` і поясни письмово, чому там **три** значення,
а не чотири.

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

```python
assert знижка_за_обсяг.__defaults__ == (1, 5, 0.1)
assert len(знижка_за_обсяг.__defaults__) == len(інспекція_параметрів) - 1
```

де `інспекція_параметрів` — список імен параметрів, який ти отримав через
`inspect.signature(знижка_за_обсяг).parameters`.

---

## 🟡 Рівень 2 — Плюс

### 2.1 · Пастка своїми руками, потім ліки

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

- `зібрати_чек_пастка` — зі значенням за замовчуванням `[]`;
- `зібрати_чек` — зі значенням `None` і створенням списку в тілі.

Виклич кожну по три рази поспіль і доведи різницю `assert`ами.

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

```python
п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`;
- `з_доданим(кошик, товар)` — не чіпає вхід і повертає **новий** список.

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

```python
вихідний = ["хліб"]
id_до = id(вихідний)

assert додати_на_місці(вихідний, "мед") is None, "функція, що змінює, нічого не повертає"
assert вихідний == ["хліб", "мед"]
assert id(вихідний) == id_до, "обʼєкт мав лишитись тим самим"

інший = ["хліб"]
новий = з_доданим(інший, "мед")
assert інший == ["хліб"], "вхід мав лишитись недоторканим"
assert новий == ["хліб", "мед"] and новий is not інший
```

і ти назвав пару функцій зі стандартної бібліотеки, влаштовану за тим самим принципом.

### 2.3 · `*args` і `**kwargs` на практиці

Напиши функцію `підсумок(*позиції, знижка=0.0, **службові)`, яка рахує суму цін
переданих товарів, застосовує знижку й **ігнорує** будь-які службові аргументи,
але друкує, які саме отримала.

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

```python
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 одночасно, а вкладена
функція повертає те, що бачить. Потім, **прибираючи** оголошення по одному, покажи,
як відповідь змінюється.

**Зроблено, якщо:** твій код друкує три різні значення в трьох різних станах і
виконується

```python
assert знайдене == ["локальне", "обгортка", "глобальне"]
```

де `знайдене` — список того, що повернула вкладена функція в кожному зі станів.
Плюс окремо покажи `NameError` для імені, якого немає ніде, і поясни, чому для
імені `sum` така помилка неможлива.

---

## 🔴 Рівень 3 — Виклик

### 3.1 · Свій розподіл аргументів проти бібліотечного

Напиши функцію `розкласти(параметри, замовчування, аргументи, ключові)`, яка **сама**
розкладає аргументи по параметрах і повертає словник «імʼя параметра → значення»:

- `параметри` — список імен у порядку оголошення;
- `замовчування` — словник «імʼя → типове значення» (може бути неповним);
- `аргументи` — кортеж позиційних;
- `ключові` — словник іменованих.

Функція має піднімати `TypeError` у трьох випадках: параметр отримав два значення,
обовʼязковий параметр не отримав нічого, переданий невідомий ключ.

**Зроблено, якщо:** твій результат збігається з `inspect.signature(...).bind(...)`
щонайменше на пʼятьох викликах, включно з помилковими:

```python
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` у розвʼязку бути не має.

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

```python
перший = зробити_лічильник()
другий = зробити_лічильник(старт=100, крок=10)

assert [перший(), перший(), перший()] == [1, 2, 3]
assert [другий(), другий()] == [110, 120]
assert перший() == 4, "лічильники не мають заважати один одному"
assert перший.__code__.co_freevars, "стан має жити в замиканні, а не в глобальній памʼяті"
```

Останній `assert` і є перевіркою на відсутність `global`: якби лічильник лежав на рівні
модуля, захоплених імен у поверненої функції не було б узагалі.

І окремо надрукуй `перший.__code__.co_freevars` та поясни одним реченням, що це за імена.

### 3.3 · Затінення, яке ламає чужий код

Напиши функцію `безпечна_довжина(послідовність)`, яка повертає `len(послідовність)`.
Потім навмисно затінь на рівні модуля імʼя `len` числом і покажи, що функція
перестала працювати, **хоча її текст не змінювався**. Далі відновіть доступ трьома
різними способами й порівняй їх.

**Зроблено, якщо:** твій код друкує повідомлення помилки й виконуються

```python
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` і чому пошук усередині функції доходить до нього раніше, ніж до вбудованих.
