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

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

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

  1. викличемо одну функцію шістьма способами й побачимо, що зійшлося, а що ні;
  2. зазирнемо в функція.__defaults__ і впіймаємо пастку змінюваного замовчування — з assert, який доводить, що список справді накопичується;
  3. полагодимо її через None і перевіримо, що обʼєкти тепер різні;
  4. побачимо різницю між «змінити обʼєкт» і «перепризначити імʼя» через id();
  5. зберемо *args і **kwargs, а потім розпакуємо список і словник у виклик;
  6. звіримо власний розподіл аргументів із бібліотечним inspect.signature().bind();
  7. пройдемо всі чотири рівні LEGB, зловимо UnboundLocalError і затінимо sum.

Кожна клітинка друкує результат — запускай згори вниз і читай, що виводиться.

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

Словник цін — той самий, що в темах 08 і 12. Він живе на рівні модуля, тобто на рівні G у LEGB: усі функції нижче зможуть його читати, нічого нікуди не передаючи.

In [1]:
ЦІНИ = {"хліб": 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.

In [2]:
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 розкладає їх по іменах, а не по місцях. Переконаймося, що всі три записи дають однакове число.

In [3]:
позиційно = оформити("мед", 3, 0.1)
іменовано = оформити("мед", кількість=3, знижка=0.1)
перемішано = оформити(знижка=0.1, товар="мед", кількість=3)

print("позиційно  :", позиційно)
print("іменовано  :", іменовано)
print("перемішано :", перемішано)

assert позиційно == іменовано == перемішано, "виклики мали б бути еквівалентні"
print("✅ усі три записи — це один і той самий виклик")
позиційно  : 391.5
іменовано  : 391.5
перемішано : 391.5
✅ усі три записи — це один і той самий виклик

4 · Іменований аргумент дозволяє «перестрибнути» середній параметр¶

Якщо знижка потрібна, а кількість — ні, позиційно це записати неможливо: довелось би явно передати кількість. З іменем — можна.

In [4]:
без_знижки = оформити("мед")
зі_знижкою = оформити("мед", знижка=0.1)

print("без знижки :", без_знижки)
print("зі знижкою :", зі_знижкою)
# кількість в обох випадках лишилась типовою одиницею
assert зі_знижкою == round(без_знижки * 0.9, 2)
print("✅ кількість узялася за замовчуванням, знижка — з імені")
без знижки : 145.0
зі знижкою : 130.5
✅ кількість узялася за замовчуванням, знижка — з імені

5 · Три способи помилитися у виклику¶

Ловимо помилки в try/except, щоб зошит не зупинявся, і друкуємо саме текст повідомлення — він майже завжди прямо називає проблемний параметр.

In [5]:
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ом. Ця клітинка падає навмисно: помилка тут і є результатом.

In [6]:
оформити(кількість=3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[6], line 1
----> 1 оформити(кількість=3)

TypeError: оформити() missing 1 required positional argument: 'товар'

6 · Значення за замовчуванням можна побачити¶

Функція — теж обʼєкт, і готові типові значення лежать у неї в атрибуті __defaults__. Це кортеж уже створених обʼєктів, а не опис «що створювати».

In [7]:
print("оформити.__defaults__ =", оформити.__defaults__)
print("тип цього атрибута    :", type(оформити.__defaults__))

assert оформити.__defaults__ == (1, 0.0)
print("✅ обидва замовчування вже існують — ще до першого виклику")
оформити.__defaults__ = (1, 0.0)
тип цього атрибута    : <class 'tuple'>
✅ обидва замовчування вже існують — ще до першого виклику

7 · Пастка: змінюване значення за замовчуванням¶

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

In [8]:
def додати_пастка(товар, кошик=[]):
    """НЕ ПИШИ ТАК. Порожній список тут створюється один раз — разом із функцією."""
    кошик.append(товар)
    return кошик


р1 = додати_пастка("яблуко")
р2 = додати_пастка("груша")
р3 = додати_пастка("слива")

print("р1 =", р1)
print("р2 =", р2)
print("р3 =", р3)
р1 = ['яблуко', 'груша', 'слива']
р2 = ['яблуко', 'груша', 'слива']
р3 = ['яблуко', 'груша', 'слива']

Кожен виклик мав повернути список з одного фрукта, а повернув усе, що накидали раніше. Доведемо, що це не три схожі списки, а буквально один обʼєкт: у нього однаковий id(), і він же лежить у __defaults__.

In [9]:
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__ = (['яблуко', 'груша', 'слива'],)
✅ пастка підтверджена: один список, три виклики, три елементи

Ще один спосіб побачити те саме: викликати функцію в циклі й дивитись, як росте довжина результату. Якби кожен виклик створював свій список, довжина завжди дорівнювала б одиниці.

In [10]:
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 незмінний і існує в єдиному примірнику — зіпсувати його неможливо. Порожній список створюємо всередині функції, тобто на кожен виклик свій.

In [11]:
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,)
✅ два виклики — два різні обʼєкти

Перевіримо ще, що функція коректно працює й тоді, коли кошик передали: вона має дописати в переданий обʼєкт, а не завести новий.

In [12]:
мій_кошик = ["хліб"]
повернуте = додати("мед", мій_кошик)

print("мій_кошик:", мій_кошик)
print("повернуте:", повернуте)
assert повернуте is мій_кошик, "функція мала дописати в переданий список"
print("✅ переданий кошик — той самий обʼєкт, що й повернутий")
мій_кошик: ['хліб', 'мед']
повернуте: ['хліб', 'мед']
✅ переданий кошик — той самий обʼєкт, що й повернутий

9 · Чому if not кошик — погана заміна if кошик is None¶

Спокуслива «коротша» перевірка спрацьовує і на порожньому переданому списку. Тоді функція мовчки викидає чужий обʼєкт і пише у свій новий.

In [13]:
def додати_недбало(товар, кошик=None):
    if not кошик:              # ПОМИЛКА: порожній список теж «неправдивий»
        кошик = []
    кошик.append(товар)
    return кошик


порожній_кошик = []
результат = додати_недбало("мед", порожній_кошик)

print("що ми передали      :", порожній_кошик)
print("що функція повернула:", результат)
assert порожній_кошик == [], "переданий список так і лишився порожнім"
assert результат is not порожній_кошик, "функція працювала з іншим обʼєктом"
print("⚠️  переданий кошик проігноровано — саме тому перевіряють is None")
що ми передали      : []
що функція повернула: ['мед']
⚠️  переданий кошик проігноровано — саме тому перевіряють is None

10 · Змінити обʼєкт проти перепризначити імʼя¶

Дві функції роблять «те саме зі списком» — і дають протилежний результат. Дивимось не на значення, а на id(): він відповідає на питання «це той самий обʼєкт?».

In [14]:
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(список).

In [15]:
def з_доданим(кошик, товар):
    """Нічого не чіпає: повертає НОВИЙ список."""
    новий = list(кошик)        # чесна копія, а не другий ярлик
    новий.append(товар)
    return новий


вихідний = ["хліб", "сіль"]
розширений = з_доданим(вихідний, "мед")

print("вихідний  :", вихідний)
print("розширений:", розширений)
assert вихідний == ["хліб", "сіль"], "вихідний список мав лишитись недоторканим"
assert розширений is not вихідний
print("✅ функція без побічних ефектів: вхід не змінився")
вихідний  : ['хліб', 'сіль']
розширений: ['хліб', 'сіль', 'мед']
✅ функція без побічних ефектів: вхід не змінився

11 · *args: скільки завгодно позиційних аргументів¶

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

In [16]:
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: скільки завгодно іменованих аргументів¶

Зайві іменовані аргументи збираються у словник: ключ — рядок з іменем параметра.

In [17]:
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.

In [18]:
позиція = ["мед", 3, 0.1]
налаштування = {"кількість": 3, "знижка": 0.1}

явно = оформити("мед", 3, 0.1)
зі_списку = оформити(*позиція)                  # три окремі аргументи
зі_словника = оформити("мед", **налаштування)   # два іменовані аргументи

print("явно        :", явно)
print("зі_списку   :", зі_списку)
print("зі_словника :", зі_словника)

assert явно == зі_списку == зі_словника
print("✅ три записи одного виклику")
явно        : 391.5
зі_списку   : 391.5
зі_словника : 391.5
✅ три записи одного виклику

Важливо не плутати f(список) і f(*список): у першому випадку функція отримає один аргумент — сам список.

In [19]:
def скільки_аргументів(*аргументи):
    return len(аргументи)


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

assert скільки_аргументів(позиція) == 1, "без зірочки список іде цілим"
assert скільки_аргументів(*позиція) == 3, "із зірочкою він розкладається"
print("✅ зірочка змінює кількість аргументів, а не їхні значення")
скільки_аргументів(позиція)  = 1
скільки_аргументів(*позиція) = 3
✅ зірочка змінює кількість аргументів, а не їхні значення

14 · Звіряємо власний розподіл із бібліотечним¶

Модуль inspect уміє робити рівно те, що робить Python при виклику: розкласти аргументи по параметрах. Порівняймо його відповідь із тим, що ми чекали руками, — і заразом підставимо замовчування через apply_defaults().

In [20]:
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.

In [21]:
for опис, аргументи, ключові in [
    ("двічі те саме", ("мед", 3), {"кількість": 2}),
    ("немає товару",  (),         {"кількість": 3}),
]:
    try:
        підпис.bind(*аргументи, **ключові)
    except TypeError as помилка:
        print(f"{опис:<15} → {помилка}")
двічі те саме   → multiple values for argument 'кількість'
немає товару    → missing a required argument: 'товар'

15 · Тільки-іменовані параметри¶

Зірочка в списку параметрів означає: усе, що правіше, можна передати лише на імʼя. Це змушує писати читабельні виклики, а не вгадувати, що означає третій True.

In [22]:
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: усі чотири рівні на одному прикладі¶

Імена однакові, значення різні. Функція внутрішня друкує те, що знайшла першим, ідучи зсередини назовні.

In [23]:
ім_я = "глобальне"          # рівень 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. Та сама помилка, що й від звичайної одруківки.

In [24]:
try:
    print(підсумок_якого_немає)
except NameError as помилка:
    print("NameError:", помилка)

# а от sum знайдеться завжди — він живе серед вбудованих
print("sum знайдено серед вбудованих:", sum)
NameError: name 'підсумок_якого_немає' is not defined
sum знайдено серед вбудованих: <built-in function sum>

17 · UnboundLocalError: присвоєння робить імʼя локальним¶

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

In [25]:
лічильник = 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 і через явне повернення результату. Обидва дають те саме число, але другий не залежить від історії викликів.

In [26]:
викликів_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).

In [27]:
def зробити_лічильник():
    """Повертає функцію, яка памʼятає, скільки разів її викликали."""
    скільки = 0

    def крок():
        nonlocal скільки       # без цього рядка був би UnboundLocalError
        скільки += 1
        return скільки

    return крок


рахує = зробити_лічильник()
інший = зробити_лічильник()

print("рахує():", рахує(), рахує(), рахує())
print("інший():", інший())
print("захоплені імена:", рахує.__code__.co_freevars)

assert рахує() == 4, "четвертий виклик того самого лічильника"
assert інший() == 2, "у другого лічильника свій власний стан"
print("✅ два лічильники не заважають один одному — стан у кожного свій")
рахує(): 1 2 3
інший(): 1
захоплені імена: ('скільки',)
✅ два лічильники не заважають один одному — стан у кожного свій

20 · Затінення вбудованого імені¶

Глобальний рівень перевіряється раніше за вбудований, тому власне імʼя перекриває однойменну функцію. Помилки при цьому немає — вона вилізе пізніше й в іншому місці.

In [28]:
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 замість списку й розпакування словника у виклик.

In [29]:
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(...) на щонайменше пʼятьох різних викликах, включно з помилковими.