Практика · Аргументи й область видимості¶
Лекція: lecture.html · Тест: quiz.html · ДЗ: homework.md
Наскрізний приклад той самий, що в лекції, — кошик покупок. Тут ми руками зробимо все, про що йшлося:
- викличемо одну функцію шістьма способами й побачимо, що зійшлося, а що ні;
- зазирнемо в
функція.__defaults__і впіймаємо пастку змінюваного замовчування — зassert, який доводить, що список справді накопичується; - полагодимо її через
Noneі перевіримо, що обʼєкти тепер різні; - побачимо різницю між «змінити обʼєкт» і «перепризначити імʼя» через
id(); - зберемо
*argsі**kwargs, а потім розпакуємо список і словник у виклик; - звіримо власний розподіл аргументів із бібліотечним
inspect.signature().bind(); - пройдемо всі чотири рівні LEGB, зловимо
UnboundLocalErrorі затінимоsum.
Кожна клітинка друкує результат — запускай згори вниз і читай, що виводиться.
1 · Дані, з якими працюємо¶
Словник цін — той самий, що в темах 08 і 12. Він живе на рівні модуля, тобто на рівні G у LEGB: усі функції нижче зможуть його читати, нічого нікуди не передаючи.
ЦІНИ = {"хліб": 28.5, "молоко": 32.0, "яблука": 19.9, "мед": 145.0, "сіль": 12.0}
print("ціни:", ЦІНИ)
print("позицій у прайсі:", len(ЦІНИ))
ціни: {'хліб': 28.5, 'молоко': 32.0, 'яблука': 19.9, 'мед': 145.0, 'сіль': 12.0}
позицій у прайсі: 5
2 · Функція з трьома параметрами¶
Два з них мають значення за замовчуванням. Параметр без замовчування (товар) стоїть
першим — інакше Python не прийняв би такий def.
def оформити(товар, кількість=1, знижка=0.0):
"""Скільки коштує позиція кошика після знижки."""
сума = ЦІНИ[товар] * кількість
# округлюємо до копійок, бо гроші не бувають із нескінченним хвостом
return round(сума * (1 - знижка), 2)
print("оформити('мед') =", оформити("мед"))
print("оформити('мед', 3) =", оформити("мед", 3))
print("оформити('мед', 3, 0.1) =", оформити("мед", 3, 0.1))
оформити('мед') = 145.0
оформити('мед', 3) = 435.0
оформити('мед', 3, 0.1) = 391.5
3 · Іменовані аргументи: той самий виклик, інша читабельність¶
Порядок іменованих аргументів не має значення взагалі — Python розкладає їх по іменах, а не по місцях. Переконаймося, що всі три записи дають однакове число.
позиційно = оформити("мед", 3, 0.1)
іменовано = оформити("мед", кількість=3, знижка=0.1)
перемішано = оформити(знижка=0.1, товар="мед", кількість=3)
print("позиційно :", позиційно)
print("іменовано :", іменовано)
print("перемішано :", перемішано)
assert позиційно == іменовано == перемішано, "виклики мали б бути еквівалентні"
print("✅ усі три записи — це один і той самий виклик")
позиційно : 391.5 іменовано : 391.5 перемішано : 391.5 ✅ усі три записи — це один і той самий виклик
4 · Іменований аргумент дозволяє «перестрибнути» середній параметр¶
Якщо знижка потрібна, а кількість — ні, позиційно це записати неможливо: довелось би явно передати кількість. З іменем — можна.
без_знижки = оформити("мед")
зі_знижкою = оформити("мед", знижка=0.1)
print("без знижки :", без_знижки)
print("зі знижкою :", зі_знижкою)
# кількість в обох випадках лишилась типовою одиницею
assert зі_знижкою == round(без_знижки * 0.9, 2)
print("✅ кількість узялася за замовчуванням, знижка — з імені")
без знижки : 145.0 зі знижкою : 130.5 ✅ кількість узялася за замовчуванням, знижка — з імені
5 · Три способи помилитися у виклику¶
Ловимо помилки в try/except, щоб зошит не зупинявся, і друкуємо саме текст
повідомлення — він майже завжди прямо називає проблемний параметр.
try:
оформити("мед", 3, кількість=2) # трійка й «кількість=2» претендують на один параметр
except TypeError as помилка:
print("двічі те саме →", помилка)
try:
оформити(кількість=3) # обовʼязковий «товар» ніхто не передав
except TypeError as помилка:
print("немає товару →", помилка)
try:
оформити("мед", вага=2) # такого параметра у функції взагалі немає
except TypeError as помилка:
print("зайвий аргумент →", помилка)
try:
оформити("мед", 3, 0.1, 5) # позиційних аргументів більше, ніж параметрів
except TypeError as помилка:
print("забагато значень→", помилка)
двічі те саме → оформити() got multiple values for argument 'кількість' немає товару → оформити() missing 1 required positional argument: 'товар' зайвий аргумент → оформити() got an unexpected keyword argument 'вага' забагато значень→ оформити() takes from 1 to 3 positional arguments but 4 were given
А ось як та сама помилка виглядає без try — зі справжнім tracebackом.
Ця клітинка падає навмисно: помилка тут і є результатом.
оформити(кількість=3)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[6], line 1 ----> 1 оформити(кількість=3) TypeError: оформити() missing 1 required positional argument: 'товар'
6 · Значення за замовчуванням можна побачити¶
Функція — теж обʼєкт, і готові типові значення лежать у неї в атрибуті __defaults__.
Це кортеж уже створених обʼєктів, а не опис «що створювати».
print("оформити.__defaults__ =", оформити.__defaults__)
print("тип цього атрибута :", type(оформити.__defaults__))
assert оформити.__defaults__ == (1, 0.0)
print("✅ обидва замовчування вже існують — ще до першого виклику")
оформити.__defaults__ = (1, 0.0) тип цього атрибута : <class 'tuple'> ✅ обидва замовчування вже існують — ще до першого виклику
7 · Пастка: змінюване значення за замовчуванням¶
Функція виглядає бездоганно: «якщо кошика не передали — візьми порожній». Викличемо її тричі й подивимось, що вона поверне.
def додати_пастка(товар, кошик=[]):
"""НЕ ПИШИ ТАК. Порожній список тут створюється один раз — разом із функцією."""
кошик.append(товар)
return кошик
р1 = додати_пастка("яблуко")
р2 = додати_пастка("груша")
р3 = додати_пастка("слива")
print("р1 =", р1)
print("р2 =", р2)
print("р3 =", р3)
р1 = ['яблуко', 'груша', 'слива'] р2 = ['яблуко', 'груша', 'слива'] р3 = ['яблуко', 'груша', 'слива']
Кожен виклик мав повернути список з одного фрукта, а повернув усе, що накидали раніше.
Доведемо, що це не три схожі списки, а буквально один обʼєкт: у нього однаковий id(),
і він же лежить у __defaults__.
print("id(р1) =", id(р1))
print("id(р2) =", id(р2))
print("id(р3) =", id(р3))
print("додати_пастка.__defaults__ =", додати_пастка.__defaults__)
# головна перевірка теми: пастка справді НАКОПИЧУЄ між викликами
assert len(р3) == 3, "після трьох викликів у списку мало б бути три елементи"
assert р1 is р2 is р3, "усі три результати — це один і той самий обʼєкт"
assert додати_пастка.__defaults__[0] is р1, "той самий список лежить у __defaults__"
assert додати_пастка.__defaults__ == (["яблуко", "груша", "слива"],)
print("✅ пастка підтверджена: один список, три виклики, три елементи")
id(р1) = 140478276086336 id(р2) = 140478276086336 id(р3) = 140478276086336 додати_пастка.__defaults__ = (['яблуко', 'груша', 'слива'],) ✅ пастка підтверджена: один список, три виклики, три елементи
Ще один спосіб побачити те саме: викликати функцію в циклі й дивитись, як росте довжина результату. Якби кожен виклик створював свій список, довжина завжди дорівнювала б одиниці.
def свіжа_пастка(товар, кошик=[]):
кошик.append(товар)
return кошик
довжини = []
for номер in range(1, 6):
результат = свіжа_пастка(f"товар-{номер}")
довжини.append(len(результат))
print("довжина результату після кожного виклику:", довжини)
assert довжини == [1, 2, 3, 4, 5], "довжина мала б рости на одиницю щоразу"
print("✅ список росте — бо він один на всі виклики")
довжина результату після кожного виклику: [1, 2, 3, 4, 5] ✅ список росте — бо він один на всі виклики
8 · Як писати правильно: None замість списку¶
None незмінний і існує в єдиному примірнику — зіпсувати його неможливо.
Порожній список створюємо всередині функції, тобто на кожен виклик свій.
def додати(товар, кошик=None):
"""Правильний варіант: типове значення незмінне, список створюємо в тілі."""
if кошик is None: # питання саме «чи передали», а не «чи порожній»
кошик = []
кошик.append(товар)
return кошик
д1 = додати("яблуко")
д2 = додати("груша")
print("д1 =", д1, "| id:", id(д1))
print("д2 =", д2, "| id:", id(д2))
print("додати.__defaults__ =", додати.__defaults__)
assert д1 == ["яблуко"] and д2 == ["груша"]
assert д1 is not д2, "кожен виклик мав створити власний список"
assert додати.__defaults__ == (None,), "None зіпсувати неможливо"
print("✅ два виклики — два різні обʼєкти")
д1 = ['яблуко'] | id: 140478264650624 д2 = ['груша'] | id: 140478264653696 додати.__defaults__ = (None,) ✅ два виклики — два різні обʼєкти
Перевіримо ще, що функція коректно працює й тоді, коли кошик передали: вона має дописати в переданий обʼєкт, а не завести новий.
мій_кошик = ["хліб"]
повернуте = додати("мед", мій_кошик)
print("мій_кошик:", мій_кошик)
print("повернуте:", повернуте)
assert повернуте is мій_кошик, "функція мала дописати в переданий список"
print("✅ переданий кошик — той самий обʼєкт, що й повернутий")
мій_кошик: ['хліб', 'мед'] повернуте: ['хліб', 'мед'] ✅ переданий кошик — той самий обʼєкт, що й повернутий
9 · Чому if not кошик — погана заміна if кошик is None¶
Спокуслива «коротша» перевірка спрацьовує і на порожньому переданому списку. Тоді функція мовчки викидає чужий обʼєкт і пише у свій новий.
def додати_недбало(товар, кошик=None):
if not кошик: # ПОМИЛКА: порожній список теж «неправдивий»
кошик = []
кошик.append(товар)
return кошик
порожній_кошик = []
результат = додати_недбало("мед", порожній_кошик)
print("що ми передали :", порожній_кошик)
print("що функція повернула:", результат)
assert порожній_кошик == [], "переданий список так і лишився порожнім"
assert результат is not порожній_кошик, "функція працювала з іншим обʼєктом"
print("⚠️ переданий кошик проігноровано — саме тому перевіряють is None")
що ми передали : [] що функція повернула: ['мед'] ⚠️ переданий кошик проігноровано — саме тому перевіряють is None
10 · Змінити обʼєкт проти перепризначити імʼя¶
Дві функції роблять «те саме зі списком» — і дають протилежний результат.
Дивимось не на значення, а на id(): він відповідає на питання «це той самий обʼєкт?».
def дописати(список):
"""Змінює обʼєкт у місці — зміну видно ззовні."""
список.append("мед")
def замінити(список):
"""Перечіпляє ЛОКАЛЬНЕ імʼя на новий обʼєкт — ззовні нічого не зміниться."""
список = ["мед"]
кошик_а = ["хліб"]
кошик_б = ["хліб"]
id_до_а, id_до_б = id(кошик_а), id(кошик_б)
дописати(кошик_а)
замінити(кошик_б)
print("після дописати() :", кошик_а, "| id той самий:", id(кошик_а) == id_до_а)
print("після замінити() :", кошик_б, "| id той самий:", id(кошик_б) == id_до_б)
assert кошик_а == ["хліб", "мед"], "append мав змінити зовнішній список"
assert кошик_б == ["хліб"], "присвоєння всередині функції ззовні не видно"
assert id(кошик_а) == id_до_а and id(кошик_б) == id_до_б
print("✅ обидва імені досі вказують на свої початкові обʼєкти")
після дописати() : ['хліб', 'мед'] | id той самий: True після замінити() : ['хліб'] | id той самий: True ✅ обидва імені досі вказують на свої початкові обʼєкти
Як писати так, щоб не було непорозумінь: або функція змінює й повертає None,
або не чіпає аргумент і повертає новий список. Так само влаштована й сама мова:
список.sort() проти sorted(список).
def з_доданим(кошик, товар):
"""Нічого не чіпає: повертає НОВИЙ список."""
новий = list(кошик) # чесна копія, а не другий ярлик
новий.append(товар)
return новий
вихідний = ["хліб", "сіль"]
розширений = з_доданим(вихідний, "мед")
print("вихідний :", вихідний)
print("розширений:", розширений)
assert вихідний == ["хліб", "сіль"], "вихідний список мав лишитись недоторканим"
assert розширений is not вихідний
print("✅ функція без побічних ефектів: вхід не змінився")
вихідний : ['хліб', 'сіль'] розширений: ['хліб', 'сіль', 'мед'] ✅ функція без побічних ефектів: вхід не змінився
11 · *args: скільки завгодно позиційних аргументів¶
Зайві позиційні аргументи збираються в кортеж. Напишемо власну суму й звіримо її
з вбудованою sum() — щоб побачити, що всередині бібліотечної функції немає магії.
def сума_усіх(*числа):
"""Приймає скільки завгодно чисел; усі зайві падають у кортеж «числа»."""
разом = 0
for число in числа:
разом += число
return разом
def показати_зібране(*числа):
"""Нічого не рахує — просто повертає те, що зібралось у кортеж."""
return числа
print("у *числа збирається :", показати_зібране(10, 20, 5),
"— тип", type(показати_зібране(10, 20, 5)).__name__)
print("сума_усіх() =", сума_усіх())
print("сума_усіх(10) =", сума_усіх(10))
print("сума_усіх(10, 20, 5) =", сума_усіх(10, 20, 5))
assert сума_усіх(10, 20, 5) == sum([10, 20, 5]), "розрахунок розійшовся з sum()"
assert сума_усіх() == sum([]) == 0, "без аргументів кортеж порожній, сума нульова"
print("✅ збігається з вбудованою sum()")
у *числа збирається : (10, 20, 5) — тип tuple сума_усіх() = 0 сума_усіх(10) = 10 сума_усіх(10, 20, 5) = 35 ✅ збігається з вбудованою sum()
12 · **kwargs: скільки завгодно іменованих аргументів¶
Зайві іменовані аргументи збираються у словник: ключ — рядок з іменем параметра.
def звіт(назва, *решта, **опції):
"""Показує, що саме куди потрапило."""
return {"назва": назва, "решта": решта, "опції": опції}
порожній = звіт("чек")
повний = звіт("чек", 10, 20, гучно=True, файл="чек.txt")
print("звіт('чек') →", порожній)
print("звіт('чек', 10, ...) →", повний)
assert порожній["решта"] == () and порожній["опції"] == {}, "порожні — це () і {}, не None"
assert повний["решта"] == (10, 20)
assert повний["опції"] == {"гучно": True, "файл": "чек.txt"}
assert isinstance(повний["решта"], tuple) and isinstance(повний["опції"], dict)
print("✅ зайве позиційне — у кортеж, зайве іменоване — у словник")
звіт('чек') → {'назва': 'чек', 'решта': (), 'опції': {}}
звіт('чек', 10, ...) → {'назва': 'чек', 'решта': (10, 20), 'опції': {'гучно': True, 'файл': 'чек.txt'}}
✅ зайве позиційне — у кортеж, зайве іменоване — у словник
13 · Ті самі зірочки при виклику: розпакування¶
У рядку виклику зірочки не збирають, а розкладають. Це прямий родич розпакування кортежів із теми 07.
позиція = ["мед", 3, 0.1]
налаштування = {"кількість": 3, "знижка": 0.1}
явно = оформити("мед", 3, 0.1)
зі_списку = оформити(*позиція) # три окремі аргументи
зі_словника = оформити("мед", **налаштування) # два іменовані аргументи
print("явно :", явно)
print("зі_списку :", зі_списку)
print("зі_словника :", зі_словника)
assert явно == зі_списку == зі_словника
print("✅ три записи одного виклику")
явно : 391.5 зі_списку : 391.5 зі_словника : 391.5 ✅ три записи одного виклику
Важливо не плутати f(список) і f(*список): у першому випадку функція отримає
один аргумент — сам список.
def скільки_аргументів(*аргументи):
return len(аргументи)
print("скільки_аргументів(позиція) =", скільки_аргументів(позиція))
print("скільки_аргументів(*позиція) =", скільки_аргументів(*позиція))
assert скільки_аргументів(позиція) == 1, "без зірочки список іде цілим"
assert скільки_аргументів(*позиція) == 3, "із зірочкою він розкладається"
print("✅ зірочка змінює кількість аргументів, а не їхні значення")
скільки_аргументів(позиція) = 1 скільки_аргументів(*позиція) = 3 ✅ зірочка змінює кількість аргументів, а не їхні значення
14 · Звіряємо власний розподіл із бібліотечним¶
Модуль inspect уміє робити рівно те, що робить Python при виклику: розкласти
аргументи по параметрах. Порівняймо його відповідь із тим, що ми чекали руками, —
і заразом підставимо замовчування через apply_defaults().
import inspect
підпис = inspect.signature(оформити)
print("підпис функції:", підпис)
розподіл = підпис.bind("мед", знижка=0.1) # те саме, що оформити("мед", знижка=0.1)
розподіл.apply_defaults() # дописуємо те, чого не передали
наше_очікування = {"товар": "мед", "кількість": 1, "знижка": 0.1}
print("як лягли аргументи:", dict(розподіл.arguments))
assert dict(розподіл.arguments) == наше_очікування, "розподіл розійшовся з очікуваним"
print("✅ наш розбір збігається з бібліотечним inspect")
підпис функції: (товар, кількість=1, знижка=0.0)
як лягли аргументи: {'товар': 'мед', 'кількість': 1, 'знижка': 0.1}
✅ наш розбір збігається з бібліотечним inspect
Той самий bind ловить і помилки — саме ті, які ми бачили в розділі 5.
for опис, аргументи, ключові in [
("двічі те саме", ("мед", 3), {"кількість": 2}),
("немає товару", (), {"кількість": 3}),
]:
try:
підпис.bind(*аргументи, **ключові)
except TypeError as помилка:
print(f"{опис:<15} → {помилка}")
двічі те саме → multiple values for argument 'кількість' немає товару → missing a required argument: 'товар'
15 · Тільки-іменовані параметри¶
Зірочка в списку параметрів означає: усе, що правіше, можна передати лише на імʼя.
Це змушує писати читабельні виклики, а не вгадувати, що означає третій True.
def нарахувати(сума, *, ставка=0.05, щомісяця=False):
"""Ставка й прапорець — тільки іменовані: інакше виклик неможливо прочитати."""
множник = 1 + ставка / (12 if щомісяця else 1)
return round(сума * множник, 2)
print("нарахувати(1000) =", нарахувати(1000))
print("нарахувати(1000, ставка=0.1) =", нарахувати(1000, ставка=0.1))
print("нарахувати(1000, щомісяця=True) =", нарахувати(1000, щомісяця=True))
try:
нарахувати(1000, 0.1) # спроба передати ставку позиційно
except TypeError as помилка:
print("позиційно не можна →", помилка)
нарахувати(1000) = 1050.0 нарахувати(1000, ставка=0.1) = 1100.0 нарахувати(1000, щомісяця=True) = 1004.17 позиційно не можна → нарахувати() takes 1 positional argument but 2 were given
16 · LEGB: усі чотири рівні на одному прикладі¶
Імена однакові, значення різні. Функція внутрішня друкує те, що знайшла першим,
ідучи зсередини назовні.
ім_я = "глобальне" # рівень G — модуль
def зовнішня():
ім_я = "обгортка" # рівень E — памʼять зовнішньої функції
def внутрішня_локальна():
ім_я = "локальне" # рівень L — памʼять цього виклику
return ім_я
def внутрішня_без_свого():
return ім_я # свого немає → підніметься на рівень E
return внутрішня_локальна(), внутрішня_без_свого()
з_локальним, з_обгортки = зовнішня()
print("знайдено на L:", з_локальним)
print("знайдено на E:", з_обгортки)
print("знайдено на G:", ім_я)
print("знайдено на B:", len) # вбудоване імʼя, ніде не оголошене
assert (з_локальним, з_обгортки, ім_я) == ("локальне", "обгортка", "глобальне")
print("✅ перший знайдений рівень виграє, решта не розглядається")
знайдено на L: локальне знайдено на E: обгортка знайдено на G: глобальне знайдено на B: <built-in function len> ✅ перший знайдений рівень виграє, решта не розглядається
Імені, якого немає на жодному з чотирьох рівнів, не існує — і це NameError.
Та сама помилка, що й від звичайної одруківки.
try:
print(підсумок_якого_немає)
except NameError as помилка:
print("NameError:", помилка)
# а от sum знайдеться завжди — він живе серед вбудованих
print("sum знайдено серед вбудованих:", sum)
NameError: name 'підсумок_якого_немає' is not defined sum знайдено серед вбудованих: <built-in function sum>
17 · UnboundLocalError: присвоєння робить імʼя локальним¶
Присвоєння будь-де в тілі робить імʼя локальним для всієї функції — від першого рядка, а не з місця присвоєння. Тому наївний лічильник падає.
лічильник = 0
def збільшити_наївно():
лічильник = лічильник + 1 # праворуч Python шукає ЛОКАЛЬНЕ імʼя
return лічильник
try:
збільшити_наївно()
except UnboundLocalError as помилка:
print("UnboundLocalError:", помилка)
print("глобальний лічильник не змінився:", лічильник)
assert лічильник == 0
UnboundLocalError: cannot access local variable 'лічильник' where it is not associated with a value глобальний лічильник не змінився: 0
18 · global працює — і саме тому небезпечний¶
Порівняймо два способи порахувати виклики: через global і через явне повернення
результату. Обидва дають те саме число, але другий не залежить від історії викликів.
викликів_global = 0
def порахувати_через_global():
global викликів_global
викликів_global += 1
def порахувати_чесно(лічильник):
"""Стан приходить аргументом і йде назад результатом — жодних побічних ефектів."""
return лічильник + 1
for _ in range(3):
порахувати_через_global()
викликів_чесно = 0
for _ in range(3):
викликів_чесно = порахувати_чесно(викликів_чесно)
print("через global:", викликів_global)
print("чесно :", викликів_чесно)
assert викликів_global == викликів_чесно == 3
# ключова різниця: чесну функцію можна перевірити, не знаючи історії
assert порахувати_чесно(0) == 1 and порахувати_чесно(10) == 11
print("✅ друга функція завжди дає той самий результат на тих самих аргументах")
через global: 3 чесно : 3 ✅ друга функція завжди дає той самий результат на тих самих аргументах
19 · nonlocal: запис на рівень обгортки¶
nonlocal спрямовує присвоєння не на модуль, а на найближчу зовнішню функцію.
Стан лишається в маленькому приватному закутку — це основа замикань (тема 31).
def зробити_лічильник():
"""Повертає функцію, яка памʼятає, скільки разів її викликали."""
скільки = 0
def крок():
nonlocal скільки # без цього рядка був би UnboundLocalError
скільки += 1
return скільки
return крок
рахує = зробити_лічильник()
інший = зробити_лічильник()
print("рахує():", рахує(), рахує(), рахує())
print("інший():", інший())
print("захоплені імена:", рахує.__code__.co_freevars)
assert рахує() == 4, "четвертий виклик того самого лічильника"
assert інший() == 2, "у другого лічильника свій власний стан"
print("✅ два лічильники не заважають один одному — стан у кожного свій")
рахує(): 1 2 3
інший(): 1
захоплені імена: ('скільки',)
✅ два лічильники не заважають один одному — стан у кожного свій
20 · Затінення вбудованого імені¶
Глобальний рівень перевіряється раніше за вбудований, тому власне імʼя перекриває однойменну функцію. Помилки при цьому немає — вона вилізе пізніше й в іншому місці.
print("до затінення :", sum([1, 2, 3]))
sum = 0 # цілком дозволений рядок — і в цьому вся біда
try:
sum([1, 2, 3])
except TypeError as помилка:
print("після sum = 0:", помилка)
del sum # прибираємо ярлик із глобального рівня
print("після del sum:", sum([1, 2, 3]))
assert sum([1, 2, 3]) == 6, "вбудована функція має знову працювати"
print("✅ вбудована функція нікуди не зникала — її просто затінили")
до затінення : 6 після sum = 0: 'int' object is not callable після del sum: 6 ✅ вбудована функція нікуди не зникала — її просто затінили
21 · Складаємо все разом: чек із розпакуванням¶
Останній приклад збирає докупи все, що вивчили: іменовані аргументи, значення за
замовчуванням, None замість списку й розпакування словника у виклик.
def порахувати_чек(позиції, *, знижка_на_все=0.0, рядки=None):
"""Рахує суму кошика. Список рядків створюємо в тілі — жодних пасток."""
if рядки is None:
рядки = []
до_сплати = 0.0
for опис in позиції:
ціна = оформити(**опис) # словник розкладається на іменовані аргументи
до_сплати += ціна
рядки.append(f"{опис['товар']} → {ціна:.2f}")
return round(до_сплати * (1 - знижка_на_все), 2), рядки
кошик = [
{"товар": "хліб", "кількість": 2},
{"товар": "мед", "знижка": 0.1},
{"товар": "сіль", "кількість": 3},
]
сума, розпис = порахувати_чек(кошик, знижка_на_все=0.05)
for рядок in розпис:
print(рядок)
print("до сплати зі знижкою 5%:", сума)
очікувано = round((28.5 * 2 + 145.0 * 0.9 + 12.0 * 3) * 0.95, 2)
assert сума == очікувано, "сума розійшлася з ручним розрахунком"
assert len(розпис) == 3
# і найголовніше: другий виклик не бачить рядків першого
сума2, розпис2 = порахувати_чек(кошик)
assert len(розпис2) == 3, "пастки замовчування тут немає"
print("✅ другий виклик отримав свій власний список рядків")
хліб → 57.00 мед → 130.50 сіль → 36.00 до сплати зі знижкою 5%: 212.32 ✅ другий виклик отримав свій власний список рядків
Що далі — завдання трьох рівнів¶
Повне домашнє завдання з критеріями «зроблено» — у homework.md. Коротко, щоб не закривати зошит без справи:
🟢 Рівень 1. Напиши функцію знижка_за_обсяг(товар, кількість=1, поріг=5, відсоток=0.1),
яка дає знижку лише тоді, коли кількість не менша за поріг. Перевір її чотирма
викликами: позиційним, іменованим, без необовʼязкових аргументів і з розпакуванням
словника. Кожен результат підтверди assert.
🟡 Рівень 2. Напиши функцію з пасткою змінюваного замовчування навмисно, доведи
assertами, що вона накопичує (як у розділі 7), потім полагодь її через None і доведи
assertами, що обʼєкти тепер різні. Порівняй __defaults__ до й після трьох викликів
в обох версіях.
🔴 Рівень 3. Напиши функцію виклик_описом(функція, аргументи, ключові), яка
викликає будь-яку функцію через * і **, а перед цим сама перевіряє, чи зійдуться
аргументи, — і звір свою перевірку з inspect.signature(функція).bind(...) на
щонайменше пʼятьох різних викликах, включно з помилковими.