Практика · Функції¶

Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md

Наскрізний приклад той самий, що в лекції й у темі 12, — кошик покупок. Тут ми руками зробимо все, про що йшлося:

  1. напишемо першу функцію й переконаємось, що def нічого не виконує;
  2. побачимо різницю між параметром і аргументом — на справжньому TypeError;
  3. покладемо поруч return і print і зловимо None власними руками;
  4. напишемо функцію з кількома return і перевіримо кожну гілку;
  5. перепишемо драбинку if на ранню відмову й доведемо, що поведінка та сама;
  6. додамо докстрінги й прочитаємо їх через help();
  7. розріжемо довгий суцільний скрипт на чотири функції — і звіримо результат;
  8. переконаємось, що функція — це обʼєкт, і повернемо кілька значень кортежем.

Кожен висновок закріплюємо через assert. Якщо жодна клітинка не впала — усе зійшлось.

1 · Дані, з якими працюємо¶

Ті самі три структури, що в темі 12: список назв, словник цін і список кількостей. Порядок у списку назв і в списку кількостей узгоджений — на це ми спиратимемось у zip.

In [1]:
покупки = ["хліб", "молоко", "яблука", "мед", "сіль"]
ціни = {"хліб": 28.5, "молоко": 32.0, "яблука": 19.9, "мед": 145.0, "сіль": 12.0}
кількості = [2, 1, 3, 1, 1]

print("покупки  :", покупки)
print("ціни     :", ціни)
print("кількості:", кількості)
print("позицій у кошику:", len(покупки))
покупки  : ['хліб', 'молоко', 'яблука', 'мед', 'сіль']
ціни     : {'хліб': 28.5, 'молоко': 32.0, 'яблука': 19.9, 'мед': 145.0, 'сіль': 12.0}
кількості: [2, 1, 3, 1, 1]
позицій у кошику: 5

2 · Перша функція¶

Формула «ціна × кількість, потім знижка 10%, потім округлення до копійок» — це три дії. Дамо їм спільне імʼя.

In [2]:
def підсумок(ціна, кількість):
    разом = ціна * кількість
    разом = разом * 0.9          # знижка 10% — саме цей рядок ми хочемо мати в ОДНОМУ місці
    return round(разом, 2)


# виклик — імʼя з дужками; результат повертається сюди, у рядок print
print("хліб, 2 шт:", підсумок(28.5, 2))
print("мед,  1 шт:", підсумок(145.0, 1))
хліб, 2 шт: 51.3
мед,  1 шт: 130.5

3 · Рядок def не виконує тіло¶

Найчастіше непорозуміння новачків. Визначення функції створює обʼєкт і чіпляє на нього ярлик — і все. Тіло чекає першого виклику. Переконаймось: усередині функції стоїть print, але доки її не викликали, на екрані нічого від неї немає.

In [3]:
print("1. до визначення")


def привітати(імʼя):
    print("   привіт, " + імʼя)   # цей рядок поки НЕ виконався жодного разу


print("2. функцію визначено, а її print ще мовчить")
print("3. тип обʼєкта, на який вказує імʼя:", type(привітати))

привітати("Оля")                  # ось тепер тіло нарешті спрацювало
print("4. виклик відпрацював")
1. до визначення
2. функцію визначено, а її print ще мовчить
3. тип обʼєкта, на який вказує імʼя: <class 'function'>
   привіт, Оля
4. виклик відпрацював

4 · Параметр і аргумент¶

Параметр — імʼя в дужках у рядку def. Аргумент — конкретне значення, яке підставили при виклику. Побачити межу між ними найлегше на помилці: дамо менше аргументів, ніж оголошено параметрів.

Клітинка нижче навмисно падає — traceback тут і є навчальним матеріалом.

In [4]:
підсумок(28.5)   # параметрів два, аргумент один
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 підсумок(28.5)   # параметрів два, аргумент один

TypeError: підсумок() missing 1 required positional argument: 'кількість'

Прочитай повідомлення буквально: «бракує одного обовʼязкового позиційного аргументу — кількість». Слово positional означає, що аргументи розподіляються за позицією: перший — у перший параметр, другий — у другий.

In [5]:
# порядок аргументів має значення: обидва виклики законні, але сенс у них різний
print("підсумок(28.5, 2) — ціна 28.5, беремо 2 шт:", підсумок(28.5, 2))
print("підсумок(2, 28.5) — ціна 2.0, беремо 28.5 шт:", підсумок(2, 28.5))
print("числа збіглись випадково — множення комутативне, а от сенс різний")
підсумок(28.5, 2) — ціна 28.5, беремо 2 шт: 51.3
підсумок(2, 28.5) — ціна 2.0, беремо 28.5 шт: 51.3
числа збіглись випадково — множення комутативне, а от сенс різний

5 · return проти print — головна плутанина¶

Дві майже однакові функції. Одна віддає число програмі, друга малює його на екрані. Подивимось, що після виклику лежить в іменах.

In [6]:
def сума_A(ціна, кількість):
    return ціна * кількість      # віддає значення в програму


def сума_B(ціна, кількість):
    print(ціна * кількість)      # малює значення на екрані і НІЧОГО не повертає


x = сума_A(28.5, 2)              # на екрані нічого не зʼявиться
y = сума_B(28.5, 2)              # а тут зʼявиться 57.0 — це надрукувала сама функція

print("x =", x, "  тип:", type(x))
print("y =", y, "  тип:", type(y))
57.0
x = 57.0   тип: <class 'float'>
y = None   тип: <class 'NoneType'>

Функція без return повертає None — не «нічого», а конкретний обʼєкт-одинак типу NoneType. Закріпимо це перевірками: assert мовчить, поки все правильно, і зупиняє зошит, щойно щось розійшлось.

In [7]:
assert x == 57.0, "сума_A мала повернути число"
assert y is None, "функція без return зобовʼязана повернути None"
assert type(y).__name__ == "NoneType", "None має власний тип"

print("наступний рядок надрукує сама сума_B — її викликають прямо в assert:")
assert сума_B(2, 3) is None, "і при будь-яких інших аргументах теж None"
print("✅ функція з print замість return повертає None")
наступний рядок надрукує сама сума_B — її викликають прямо в assert:
6
✅ функція з print замість return повертає None

Тепер спробуймо скористатися результатом. З x усе гаразд — це число. А y помножити не вийде, і клітинка навмисно падає.

In [8]:
print("x * 2 =", x * 2)   # 114.0 — значення з return живе в програмі
x * 2 = 114.0
In [9]:
y * 2                     # None помножити не можна
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 y * 2                     # None помножити не можна

TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

Висновок, який економить години налагодження: функція рахує й повертає, а друкує той, хто її викликав. Пиши print(підсумок(28.5, 2)), а не ховай print усередину — тоді ту саму функцію можна і показати людині, і додати до загальної суми.

6 · Кілька return у різних гілках¶

return не лише віддає значення — він негайно завершує функцію. Тому гілки умови можна писати пласко, без жодного else.

In [10]:
def знижка(сума):
    if сума < 200:
        return 0.0
    if сума < 500:
        return 0.05
    return 0.10


# прогоняємо всі три зони, включно з межами 200 і 500
for проба in [0, 199.99, 200, 499.99, 500, 900]:
    print(f"сума {проба:>7} → знижка {знижка(проба)}")
сума       0 → знижка 0.0
сума  199.99 → знижка 0.0
сума     200 → знижка 0.05
сума  499.99 → знижка 0.05
сума     500 → знижка 0.1
сума     900 → знижка 0.1
In [11]:
assert знижка(0) == 0.0, "порожній кошик — без знижки"
assert знижка(199.99) == 0.0, "до 200 знижки немає"
assert знижка(200) == 0.05, "200 — це вже друга зона: умова сума < 200 хибна"
assert знижка(499.99) == 0.05
assert знижка(500) == 0.10, "500 — третя зона"
print("✅ усі три гілки повертають те, що обіцяли")
✅ усі три гілки повертають те, що обіцяли

А тепер — типова пастка. Приберемо останній return і подивимось, що функція поверне для великої суми. Помилки не буде: буде тихе None, яке вистрелить пізніше.

In [12]:
def знижка_зламана(сума):
    if сума < 200:
        return 0.0
    if сума < 500:
        return 0.05
    # третьої гілки немає — і для сум від 500 функція мовчки повертає None


print("знижка_зламана(100) =", знижка_зламана(100))
print("знижка_зламана(700) =", знижка_зламана(700), "← ось воно")
assert знижка_зламана(700) is None, "гілка, яка нічого не повертає, дає None"
print("✅ пастку відтворено: помилки немає, відповідь зіпсована")
знижка_зламана(100) = 0.0
знижка_зламана(700) = None ← ось воно
✅ пастку відтворено: помилки немає, відповідь зіпсована

7 · Рання відмова замість драбинки¶

Оскільки return виходить негайно, погані випадки зручно відсікати на початку. Напишемо ту саму перевірку замовлення двома способами й доведемо, що вони поводяться однаково в усіх восьми комбінаціях.

In [13]:
def оформити_вкладено(кошик, оплата, адреса):
    if кошик:
        if оплата:
            if адреса:
                return "відправлено"
            else:
                return "нема адреси"
        else:
            return "не оплачено"
    else:
        return "кошик порожній"


def оформити_рано(кошик, оплата, адреса):
    if not кошик:
        return "кошик порожній"
    if not оплата:
        return "не оплачено"
    if not адреса:
        return "нема адреси"
    return "відправлено"


print("рядків у першій версії : 11, максимальний відступ — 4 рівні")
print("рядків у другій версії :  8, максимальний відступ — 2 рівні")
рядків у першій версії : 11, максимальний відступ — 4 рівні
рядків у другій версії :  8, максимальний відступ — 2 рівні
In [14]:
# перебираємо всі 8 комбінацій трьох умов і звіряємо дві версії між собою
for кошик in (False, True):
    for оплата in (False, True):
        for адреса in (False, True):
            a = оформити_вкладено(кошик, оплата, адреса)
            b = оформити_рано(кошик, оплата, адреса)
            assert a == b, f"версії розійшлись на {кошик}, {оплата}, {адреса}"
            print(f"{кошик!s:>5} {оплата!s:>5} {адреса!s:>5} → {a}")
print("✅ обидві версії дають однакову відповідь у всіх 8 випадках")
False False False → кошик порожній
False False  True → кошик порожній
False  True False → кошик порожній
False  True  True → кошик порожній
 True False False → не оплачено
 True False  True → не оплачено
 True  True False → нема адреси
 True  True  True → відправлено
✅ обидві версії дають однакову відповідь у всіх 8 випадках

8 · Докстрінг і help()¶

Докстрінг — рядок у потрійних лапках першим у тілі функції. Python зберігає його в самій функції, і його видно через help() та атрибут __doc__. Пиши в ньому те, чого не видно з коду: що повертається, у яких одиницях, що вважається неприпустимим.

In [15]:
def вартість(ціна, кількість):
    """Повертає вартість позиції без знижки, у гривнях.

    ціна      — ціна за одиницю, число
    кількість — скільки одиниць, ціле число
    """
    return ціна * кількість


print(вартість.__doc__)
print("---")
print("перший рядок докстрінга:", вартість.__doc__.strip().split("\n")[0])
Повертає вартість позиції без знижки, у гривнях.

    ціна      — ціна за одиницю, число
    кількість — скільки одиниць, ціле число
    
---
перший рядок докстрінга: Повертає вартість позиції без знижки, у гривнях.
In [16]:
help(вартість)
print("help() показав те саме, що лежить у __doc__")
Help on function вартість in module __main__:

вартість(ціна, кількість)
    Повертає вартість позиції без знижки, у гривнях.

    ціна      — ціна за одиницю, число
    кількість — скільки одиниць, ціле число

help() показав те саме, що лежить у __doc__

9 · Ріжемо суцільний скрипт на функції¶

Ось як виглядає розрахунок чека «одним потоком»: усе злите докупи, повторно використати не можна нічого, перевірити окремо теж нічого.

In [17]:
# ── версія 1: суцільний скрипт ────────────────────────────
разом_скрипт = 0
for назва, кількість in zip(покупки, кількості):
    разом_скрипт = разом_скрипт + ціни[назва] * кількість

if разом_скрипт < 200:
    ставка = 0.0
elif разом_скрипт < 500:
    ставка = 0.05
else:
    ставка = 0.10

до_сплати_скрипт = round(разом_скрипт * (1 - ставка), 2)
print("сума без знижки:", разом_скрипт)
print("ставка знижки  :", ставка)
print("до сплати      :", до_сплати_скрипт)
сума без знижки: 305.7
ставка знижки  : 0.05
до сплати      : 290.41

Тепер той самий розрахунок, але розрізаний на чотири функції. Кожна робить одну річ і має докстрінг. Зверни увагу: жодна з них нічого не друкує — друкує той, хто викликає.

In [18]:
def вартість_позиції(ціна, кількість):
    """Повертає вартість однієї позиції: ціна за одиницю × кількість."""
    return ціна * кількість


def сума_кошика(ціни, покупки, кількості):
    """Повертає суму чека без знижки.

    ціни      — словник {назва: ціна за одиницю}
    покупки   — список назв
    кількості — список кількостей, узгоджений із покупками за порядком
    """
    разом = 0
    for назва, кількість in zip(покупки, кількості):
        разом = разом + вартість_позиції(ціни[назва], кількість)
    return разом


def ставка_знижки(сума):
    """Повертає частку знижки: 0.0, 0.05 або 0.10 залежно від суми чека."""
    if сума < 200:
        return 0.0
    if сума < 500:
        return 0.05
    return 0.10


def до_сплати(ціни, покупки, кількості):
    """Повертає підсумкову суму до сплати, округлену до копійок."""
    сума = сума_кошика(ціни, покупки, кількості)
    return round(сума * (1 - ставка_знижки(сума)), 2)


print("сума без знижки:", сума_кошика(ціни, покупки, кількості))
print("ставка знижки  :", ставка_знижки(сума_кошика(ціни, покупки, кількості)))
print("до сплати      :", до_сплати(ціни, покупки, кількості))
сума без знижки: 305.7
ставка знижки  : 0.05
до сплати      : 290.41

Головна перевірка практики: наш розрахунок = вбудований. Суму без знижки можна порахувати вбудованою sum() — якщо наша функція правильна, числа зійдуться до останнього знака.

In [19]:
наша_сума = сума_кошика(ціни, покупки, кількості)
вбудована = sum(ціни[назва] * кількість
                for назва, кількість in zip(покупки, кількості))

print("наша функція:", наша_сума)
print("через sum() :", вбудована)
assert наша_сума == вбудована, "розрахунок розійшовся з вбудованим sum()!"
assert наша_сума == разом_скрипт, "функції дали не те, що суцільний скрипт"
assert до_сплати(ціни, покупки, кількості) == до_сплати_скрипт
print("✅ збігається: суцільний скрипт, чотири функції і sum() дають одне й те саме")
наша функція: 305.7
через sum() : 305.7
✅ збігається: суцільний скрипт, чотири функції і sum() дають одне й те саме

І ось у чому виграш: кожну з чотирьох функцій можна перевірити окремо, не запускаючи весь розрахунок. Для суцільного скрипту такий рядок написати нема як.

In [20]:
assert вартість_позиції(28.5, 2) == 57.0
assert вартість_позиції(0, 100) == 0
assert ставка_знижки(199.99) == 0.0
assert ставка_знижки(305.7) == 0.05
assert сума_кошика(ціни, ["сіль"], [1]) == 12.0, "кошик з однієї позиції"
assert сума_кошика(ціни, [], []) == 0, "порожній кошик коштує нуль"
print("✅ шість перевірок окремих функцій пройдено")
✅ шість перевірок окремих функцій пройдено

10 · Функція — це обʼєкт¶

def робить те саме, що x = 5: створює обʼєкт і чіпляє на нього ярлик. Просто обʼєкт цього разу — функція. Отже, її можна присвоїти другому імені, і копії не станеться.

In [21]:
порахувати = вартість_позиції          # без дужок! це НЕ виклик, а другий ярлик

print("вартість_позиції:", вартість_позиції)
print("порахувати      :", порахувати)
print("це той самий обʼєкт?", порахувати is вартість_позиції)
print("виклик через друге імʼя:", порахувати(28.5, 2))

assert порахувати is вартість_позиції, "присвоєння не копіює функцію"
assert порахувати(28.5, 2) == вартість_позиції(28.5, 2)
print("✅ два імені, один обʼєкт-функція")
вартість_позиції: <function вартість_позиції at 0x7f0ce0996d40>
порахувати      : <function вартість_позиції at 0x7f0ce0996d40>
це той самий обʼєкт? True
виклик через друге імʼя: 57.0
✅ два імені, один обʼєкт-функція
In [22]:
del вартість_позиції                   # знімаємо ПЕРШИЙ ярлик

print("функція жива, бо на неї дивиться друге імʼя:", порахувати(28.5, 2))
assert порахувати(28.5, 2) == 57.0
print("✅ del прибрав імʼя, а не функцію")
функція жива, бо на неї дивиться друге імʼя: 57.0
✅ del прибрав імʼя, а не функцію
In [23]:
вартість_позиції(28.5, 2)   # цього імені більше немає
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[23], line 1
----> 1 вартість_позиції(28.5, 2)   # цього імені більше немає

NameError: name 'вартість_позиції' is not defined

Раз функція — звичайний обʼєкт, її можна покласти в список чи словник і дістати звідти за ключем. Так, до речі, будують «таблиці дій» замість довгих if.

In [24]:
# вертаємо звичне імʼя на місце, щоб далі зошит читався природно
вартість_позиції = порахувати

операції = {"вартість": вартість_позиції, "знижка": ставка_знижки}
print("що лежить у словнику:", операції)

for назва_дії, дія in операції.items():
    print(назва_дії, "→", дія)

print("виклик із словника:", операції["вартість"](28.5, 2))
assert операції["вартість"](28.5, 2) == 57.0
print("✅ функцію можна зберігати й діставати, як будь-яке значення")
що лежить у словнику: {'вартість': <function вартість_позиції at 0x7f0ce0996d40>, 'знижка': <function ставка_знижки at 0x7f0ce0996340>}
вартість → <function вартість_позиції at 0x7f0ce0996d40>
знижка → <function ставка_знижки at 0x7f0ce0996340>
виклик із словника: 57.0
✅ функцію можна зберігати й діставати, як будь-яке значення

11 · Повернути кілька значень¶

return віддає рівно одне значення — але цим значенням може бути кортеж. Кома сама створює кортеж, дужки не потрібні.

In [25]:
def розбір(ціни, покупки, кількості):
    """Повертає пару (сума чека, вартість найдорожчої позиції)."""
    сума = 0
    найдорожча = 0
    for назва, кількість in zip(покупки, кількості):
        позиція = ціни[назва] * кількість
        сума = сума + позиція
        if позиція > найдорожча:
            найдорожча = позиція
    return сума, найдорожча          # це кортеж


результат = розбір(ціни, покупки, кількості)
print("повернувся один обʼєкт:", результат, "  тип:", type(результат))

усього, максимум = розбір(ціни, покупки, кількості)   # розпакування з теми 07
print("усього :", усього)
print("максимум:", максимум)

assert isinstance(результат, tuple), "кома в return створює кортеж"
assert len(результат) == 2
assert усього == 305.7 and максимум == 145.0
print("✅ два значення повернулись одним кортежем і розпакувались у два імені")
повернувся один обʼєкт: (305.7, 145.0)   тип: <class 'tuple'>
усього : 305.7
максимум: 145.0
✅ два значення повернулись одним кортежем і розпакувались у два імені

12 · Чиста функція й чому її легше перевіряти¶

Чиста функція повертає однаковий результат на однакових аргументах і не змінює нічого поза собою. Усі наші розрахункові функції такі — тому кожну перевіряє один рядок. Функція, яка друкує чек, чистою не є: щоб перевірити її, довелось би перехоплювати вивід.

In [26]:
def друк_чека(ціни, покупки, кількості):
    """Друкує чек на екран. Нічого не повертає — це навмисно."""
    for назва, кількість in zip(покупки, кількості):
        рядок = вартість_позиції(ціни[назва], кількість)
        print(f"  {назва:<8} {кількість} × {ціни[назва]:>6.2f} = {рядок:>7.2f}")
    print(f"  {'до сплати':<8}                {до_сплати(ціни, покупки, кількості):>7.2f}")


поверталось = друк_чека(ціни, покупки, кількості)
print("а що повернула сама функція:", поверталось)
  хліб     2 ×  28.50 =   57.00
  молоко   1 ×  32.00 =   32.00
  яблука   3 ×  19.90 =   59.70
  мед      1 × 145.00 =  145.00
  сіль     1 ×  12.00 =   12.00
  до сплати                 290.41
а що повернула сама функція: None
In [27]:
# розрахунок і друк живуть окремо: перше перевіряється, друге — ні
assert поверталось is None, "функція, яка тільки друкує, повертає None"

# а чиста функція на однакових аргументах завжди дає однакове
перший = до_сплати(ціни, покупки, кількості)
другий = до_сплати(ціни, покупки, кількості)
assert перший == другий == 290.41, "чиста функція стабільна"

# і не псує те, що їй передали
assert покупки == ["хліб", "молоко", "яблука", "мед", "сіль"], "вхідні дані не змінились"
print("✅ до сплати:", перший, "— і список покупок лишився цілим")
✅ до сплати: 290.41 — і список покупок лишився цілим

13 · return замість прапорця¶

Типовий пошук новачок пише через змінну-прапорець. Усередині функції це зайве: return посеред циклу і виходить з циклу, і завершує функцію.

In [28]:
def є_дорожче_прапорець(ціни, межа):
    """Чи є в словнику хоч одна ціна, більша за межу. Версія з прапорцем."""
    знайдено = False
    for ціна in ціни.values():
        if ціна > межа:
            знайдено = True
            break
    return знайдено


def є_дорожче(ціни, межа):
    """Чи є в словнику хоч одна ціна, більша за межу. Версія з return у циклі."""
    for ціна in ціни.values():
        if ціна > межа:
            return True
    return False


for межа in [10, 100, 500]:
    print(f"межа {межа:>4}: прапорець → {є_дорожче_прапорець(ціни, межа)},"
          f"  return → {є_дорожче(ціни, межа)}")

for межа in [0, 10, 28.5, 100, 145, 500]:
    assert є_дорожче(ціни, межа) == є_дорожче_прапорець(ціни, межа)
print("✅ версії збігаються, але друга коротша на три рядки й без зайвого імені")
межа   10: прапорець → True,  return → True
межа  100: прапорець → True,  return → True
межа  500: прапорець → False,  return → False
✅ версії збігаються, але друга коротша на три рядки й без зайвого імені

Завдання¶

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

Напиши функцію вартість_доставки(сума), яка повертає число:

  • 0 — якщо сума чека 500 і більше (безкоштовна доставка);
  • 49 — якщо сума від 200 до 499.99;
  • 79 — якщо сума менша за 200.

Використай кілька return без жодного else. Перевір себе рядками:

assert вартість_доставки(0) == 79
assert вартість_доставки(199.99) == 79
assert вартість_доставки(200) == 49
assert вартість_доставки(499.99) == 49
assert вартість_доставки(500) == 0
print("✅ рівень 1")

Зроблено, якщо: усі пʼять assert мовчать, а у функції немає слова else.

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

Візьми функцію, яка друкує звіт по кошику, і розріж її на дві: одна рахує й повертає, друга друкує повернуте. Далі доведи, що розрахункова частина чиста:

  • виклич її двічі поспіль і порівняй результати через assert;
  • переконайся, що вона повернула не None, а число (або кортеж);
  • переконайся, що друкувальна частина повертає саме None.

Зроблено, якщо: у зошиті є три assert, які це підтверджують, і жодна розрахункова функція не містить print.

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

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

  • якщо кошик порожній — поверни (None, 0) через ранню відмову першим рядком тіла;
  • функція має бути чистою: жодного print, вхідні дані не змінюються;
  • додай докстрінг і покажи його через help().

Перевірку зроби так:

assert найдорожча_позиція(ціни, покупки, кількості) == ("мед", 145.0)
assert найдорожча_позиція(ціни, [], []) == (None, 0)
assert найдорожча_позиція(ціни, ["сіль"], [1]) == ("сіль", 12.0)
assert покупки == ["хліб", "молоко", "яблука", "мед", "сіль"]
print("✅ рівень 3")

Зроблено, якщо: усі чотири assert мовчать, перший рядок тіла — це рання відмова з return, а help(найдорожча_позиція) показує осмислений докстрінг.


Далі — тема 15: аргументи за іменем, значення за замовчуванням і область видимості. Там зʼясується, куди зникає імʼя разом після завершення функції і чому змінюваний список як значення за замовчуванням — класична пастка.