Практика · Функції¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Наскрізний приклад той самий, що в лекції й у темі 12, — кошик покупок. Тут ми руками зробимо все, про що йшлося:
- напишемо першу функцію й переконаємось, що
defнічого не виконує; - побачимо різницю між параметром і аргументом — на справжньому
TypeError; - покладемо поруч
returnіprintі зловимоNoneвласними руками; - напишемо функцію з кількома
returnі перевіримо кожну гілку; - перепишемо драбинку
ifна ранню відмову й доведемо, що поведінка та сама; - додамо докстрінги й прочитаємо їх через
help(); - розріжемо довгий суцільний скрипт на чотири функції — і звіримо результат;
- переконаємось, що функція — це обʼєкт, і повернемо кілька значень кортежем.
Кожен висновок закріплюємо через assert. Якщо жодна клітинка не впала — усе зійшлось.
1 · Дані, з якими працюємо¶
Ті самі три структури, що в темі 12: список назв, словник цін і список кількостей.
Порядок у списку назв і в списку кількостей узгоджений — на це ми спиратимемось у zip.
покупки = ["хліб", "молоко", "яблука", "мед", "сіль"]
ціни = {"хліб": 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%, потім округлення до копійок» — це три дії. Дамо їм спільне імʼя.
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, але доки її не викликали, на екрані нічого від неї немає.
print("1. до визначення")
def привітати(імʼя):
print(" привіт, " + імʼя) # цей рядок поки НЕ виконався жодного разу
print("2. функцію визначено, а її print ще мовчить")
print("3. тип обʼєкта, на який вказує імʼя:", type(привітати))
привітати("Оля") # ось тепер тіло нарешті спрацювало
print("4. виклик відпрацював")
1. до визначення 2. функцію визначено, а її print ще мовчить 3. тип обʼєкта, на який вказує імʼя: <class 'function'> привіт, Оля 4. виклик відпрацював
4 · Параметр і аргумент¶
Параметр — імʼя в дужках у рядку def. Аргумент — конкретне значення,
яке підставили при виклику. Побачити межу між ними найлегше на помилці: дамо
менше аргументів, ніж оголошено параметрів.
Клітинка нижче навмисно падає — traceback тут і є навчальним матеріалом.
підсумок(28.5) # параметрів два, аргумент один
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[4], line 1 ----> 1 підсумок(28.5) # параметрів два, аргумент один TypeError: підсумок() missing 1 required positional argument: 'кількість'
Прочитай повідомлення буквально: «бракує одного обовʼязкового позиційного аргументу —
кількість». Слово positional означає, що аргументи розподіляються за позицією:
перший — у перший параметр, другий — у другий.
# порядок аргументів має значення: обидва виклики законні, але сенс у них різний
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 — головна плутанина¶
Дві майже однакові функції. Одна віддає число програмі, друга малює його на екрані. Подивимось, що після виклику лежить в іменах.
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 мовчить, поки все правильно,
і зупиняє зошит, щойно щось розійшлось.
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 помножити не вийде, і клітинка навмисно падає.
print("x * 2 =", x * 2) # 114.0 — значення з return живе в програмі
x * 2 = 114.0
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.
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
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, яке вистрелить пізніше.
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 виходить негайно, погані випадки зручно відсікати на початку.
Напишемо ту саму перевірку замовлення двома способами й доведемо, що вони
поводяться однаково в усіх восьми комбінаціях.
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 рівні
# перебираємо всі 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__.
Пиши в ньому те, чого не видно з коду: що повертається, у яких одиницях,
що вважається неприпустимим.
def вартість(ціна, кількість):
"""Повертає вартість позиції без знижки, у гривнях.
ціна — ціна за одиницю, число
кількість — скільки одиниць, ціле число
"""
return ціна * кількість
print(вартість.__doc__)
print("---")
print("перший рядок докстрінга:", вартість.__doc__.strip().split("\n")[0])
Повертає вартість позиції без знижки, у гривнях.
ціна — ціна за одиницю, число
кількість — скільки одиниць, ціле число
---
перший рядок докстрінга: Повертає вартість позиції без знижки, у гривнях.
help(вартість)
print("help() показав те саме, що лежить у __doc__")
Help on function вартість in module __main__:
вартість(ціна, кількість)
Повертає вартість позиції без знижки, у гривнях.
ціна — ціна за одиницю, число
кількість — скільки одиниць, ціле число
help() показав те саме, що лежить у __doc__
9 · Ріжемо суцільний скрипт на функції¶
Ось як виглядає розрахунок чека «одним потоком»: усе злите докупи, повторно використати не можна нічого, перевірити окремо теж нічого.
# ── версія 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
Тепер той самий розрахунок, але розрізаний на чотири функції. Кожна робить одну річ і має докстрінг. Зверни увагу: жодна з них нічого не друкує — друкує той, хто викликає.
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() — якщо наша функція правильна, числа зійдуться
до останнього знака.
наша_сума = сума_кошика(ціни, покупки, кількості)
вбудована = sum(ціни[назва] * кількість
for назва, кількість in zip(покупки, кількості))
print("наша функція:", наша_сума)
print("через sum() :", вбудована)
assert наша_сума == вбудована, "розрахунок розійшовся з вбудованим sum()!"
assert наша_сума == разом_скрипт, "функції дали не те, що суцільний скрипт"
assert до_сплати(ціни, покупки, кількості) == до_сплати_скрипт
print("✅ збігається: суцільний скрипт, чотири функції і sum() дають одне й те саме")
наша функція: 305.7 через sum() : 305.7 ✅ збігається: суцільний скрипт, чотири функції і sum() дають одне й те саме
І ось у чому виграш: кожну з чотирьох функцій можна перевірити окремо, не запускаючи весь розрахунок. Для суцільного скрипту такий рядок написати нема як.
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: створює обʼєкт і чіпляє на нього ярлик.
Просто обʼєкт цього разу — функція. Отже, її можна присвоїти другому імені,
і копії не станеться.
порахувати = вартість_позиції # без дужок! це НЕ виклик, а другий ярлик
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 ✅ два імені, один обʼєкт-функція
del вартість_позиції # знімаємо ПЕРШИЙ ярлик
print("функція жива, бо на неї дивиться друге імʼя:", порахувати(28.5, 2))
assert порахувати(28.5, 2) == 57.0
print("✅ del прибрав імʼя, а не функцію")
функція жива, бо на неї дивиться друге імʼя: 57.0 ✅ del прибрав імʼя, а не функцію
вартість_позиції(28.5, 2) # цього імені більше немає
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[23], line 1 ----> 1 вартість_позиції(28.5, 2) # цього імені більше немає NameError: name 'вартість_позиції' is not defined
Раз функція — звичайний обʼєкт, її можна покласти в список чи словник і
дістати звідти за ключем. Так, до речі, будують «таблиці дій» замість довгих if.
# вертаємо звичне імʼя на місце, щоб далі зошит читався природно
вартість_позиції = порахувати
операції = {"вартість": вартість_позиції, "знижка": ставка_знижки}
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 віддає рівно одне значення — але цим значенням може бути кортеж.
Кома сама створює кортеж, дужки не потрібні.
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 · Чиста функція й чому її легше перевіряти¶
Чиста функція повертає однаковий результат на однакових аргументах і не змінює нічого поза собою. Усі наші розрахункові функції такі — тому кожну перевіряє один рядок. Функція, яка друкує чек, чистою не є: щоб перевірити її, довелось би перехоплювати вивід.
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
# розрахунок і друк живуть окремо: перше перевіряється, друге — ні
assert поверталось is None, "функція, яка тільки друкує, повертає None"
# а чиста функція на однакових аргументах завжди дає однакове
перший = до_сплати(ціни, покупки, кількості)
другий = до_сплати(ціни, покупки, кількості)
assert перший == другий == 290.41, "чиста функція стабільна"
# і не псує те, що їй передали
assert покупки == ["хліб", "молоко", "яблука", "мед", "сіль"], "вхідні дані не змінились"
print("✅ до сплати:", перший, "— і список покупок лишився цілим")
✅ до сплати: 290.41 — і список покупок лишився цілим
13 · return замість прапорця¶
Типовий пошук новачок пише через змінну-прапорець. Усередині функції це зайве:
return посеред циклу і виходить з циклу, і завершує функцію.
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: аргументи за іменем, значення за замовчуванням і область
видимості. Там зʼясується, куди зникає імʼя разом після завершення функції
і чому змінюваний список як значення за замовчуванням — класична пастка.