У темі 14 ми навчилися давати
шматку коду імʼя: написали def, перелічили параметри, повернули результат
через return. Функція запрацювала — і на цьому більшість підручників тему
закриває. Але між рядком виклику й рядком def насправді відбувається доволі
багато: Python має вирішити, який аргумент якому параметру дістанеться, звідки взяти те,
чого не передали, і де шукати імена, яких у функції немає взагалі. Поки все просто, ці
рішення непомітні. Щойно зʼявляється список за замовчуванням або спроба змінити зовнішню
змінну — вони стають головними.
Ця тема — про два механізми: як виклик лягає на параметри і де функція шукає імена. Обидва повністю пояснюються моделлю ярликів із теми 03, тож нічого принципово нового вчити не доведеться — доведеться довчити наслідки.
01 / ДомовленістьВиклик — це домовленість
Наскрізний приклад теми — той самий кошик покупок, що й у темі 12. Ціни лежать у словнику, а оформлення однієї позиції ми загорнули у функцію:
Спершу дві назви, які постійно плутають, хоча різниця між ними проста:
- Параметр (parameter) — імʼя в дужках у рядку
def. Тут їх три:товар,кількість,знижка. Параметр існує лише в тексті функції. - Аргумент (argument) — те, що ти реально передаєш у момент виклику:
оформити("мед", 3). Аргументів може бути менше, ніж параметрів, або більше — якщо функція таке дозволяє.
Виклик функції — це домовленість між двома сторонами. Одна сторона (def)
оголошує, що їй потрібно і що вона готова придумати сама. Друга (виклик) подає те, що має.
Python — посередник: він перевіряє, чи все зійшлося, і звʼязує кожен параметр із
конкретним обʼєктом, перш ніж виконати перший рядок тіла. Якщо звʼязати не
вдалося, тіло не виконується взагалі — помилка виникає ще до першого рядка.
TypeError, і вона зупиняє програму до входу у функцію: жодної
напівроботи, жодних зіпсованих даних. Повідомлення майже завжди прямо називає
проблемний параметр.02 / РозподілПозиційні й іменовані аргументи
Передати аргумент можна двома способами, і вони вільно змішуються в одному виклику.
Позиційно — просто значення, без імені: оформити("мед", 3).
Тут усе вирішує порядок: перший аргумент дістається першому параметру, другий — другому.
Переставиш місцями — Python не помітить підміни й порахує три меди по ціні… ні, він
спробує знайти в словнику ключ 3 і впаде з KeyError. Це ще
щасливий випадок: коли обидва аргументи числа, помилка мовчки дасть неправильну відповідь.
Іменовано (keyword argument) — з явною вказівкою параметра:
оформити(товар="мед", кількість=3). Тут порядок не має значення взагалі:
оформити(кількість=3, товар="мед") — це той самий виклик. Python розкладає
аргументи по іменах, а не по місцях.
Правило змішування одне й воно жорстке: усі позиційні аргументи стоять перед
усіма іменованими. Інакше Python навіть не почне виконувати файл — це
SyntaxError, помилка розбору, а не виконання.
Інтерактив 1 · Як виклик лягає на параметри
Обери варіант виклику — і дивись, який аргумент якому параметру дістався і звідки.
Коли варто писати імена, а коли зайве? Орієнтир такий: перший аргумент майже завжди зрозумілий із назви функції, а третій — майже ніколи. Порівняй два рядки, які роблять одне й те саме:
Перший рядок неможливо прочитати без документації. Що таке True? А
False? Другий читається як речення. Витрати — двадцять зайвих символів, зиск —
той, хто читатиме код через півроку (найімовірніше, ти сам), не полізе шукати
def. Практичне правило: булеві прапорці й числа-налаштування завжди
іменуй, а те, з чим функція працює по суті, можна лишити позиційним.
03 / ЗамовчуванняЗначення за замовчуванням
Параметр із = у рядку def має значення за
замовчуванням (default value). Це відповідь на питання «а що робити, якщо цього
аргументу не передали?». Функція оформити має два таких параметри:
кількість=1 і знижка=0.0 — розумні припущення, які
влаштовують більшість викликів.
Тут діє одне синтаксичне обмеження: параметри без замовчування йдуть першими. Інакше Python не зміг би розкласти позиційні аргументи однозначно, тому забороняє такий запис одразу:
Читається дослівно: «параметр без замовчування йде після параметра із замовчуванням». Виправлення очевидне — поміняти їх місцями.
Значення за замовчуванням — не абстракція, його можна побачити. Кожна функція в
Python — теж обʼєкт (памʼятаєш, у темі 03 ми казали, що обʼєктом є усе?), і в неї
є атрибут __defaults__: кортеж уже готових обʼєктів.
Зверни увагу на слово «уже готових». Кортеж існує до першого виклику. Це не опис «що створювати», це самі обʼєкти, які лежать у функції й чекають. Саме цей факт зараз усе й пояснить.
04 / ПасткаЗмінюване значення за замовчуванням
Ми коротко зачепили цю пастку в темі 03. Тепер розберемо її до дна: це найвідоміша пастка мови, і питання про неї трапляється на співбесідах саме тому, що відповідь показує, чи розуміє людина модель обʼєктів.
Задача побутова: функція додає товар у кошик, а якщо кошика немає — заводить новий порожній. Запис проситься сам:
Читається бездоганно: «кошик за замовчуванням порожній». Перевіримо:
Другий виклик повернув два елементи, третій — три. Функція памʼятає все, що їй колись давали, хоча ми ніде не просили її нічого запамʼятовувати.
Чому так
Розберемо по кроках, і кожен крок — уже знайома тобі річ.
Рядок def не «описує» функцію для компілятора. Він
виконується: Python створює обʼєкт-функцію й чіпляє на нього імʼя
додати — точно так само, як x = 5 створює число й чіпляє імʼя.
Щоб створити обʼєкт-функцію, Python має обчислити всі значення за замовчуванням
негайно — і скласти з них кортеж __defaults__. Отже, порожній
список [] створюється один-єдиний раз, у момент читання
def.
Коли аргумент кошик не передали, Python бере готовий обʼєкт із
__defaults__ і чіпляє на нього локальне імʼя кошик. Це
звичайне звʼязування імені — жодного копіювання. Список у функції й список у
__defaults__ — той самий обʼєкт.
А тепер згадай поділ із теми 03: append не створює нового списку, він
редагує наявний. Тобто дописує просто в той обʼєкт, що лежить у
__defaults__ і чекатиме там наступного виклику.
Ніякої магії. Одне звичайне правило («def виконується один раз») плюс
друге звичайне правило («змінюваний обʼєкт редагується в місці») дають разом поведінку,
якої ніхто не замовляв. Натисни «Викликати» кілька разів і подивись на адреси:
Інтерактив 2 · Список за замовчуванням живе між викликами
Перемикай «пастка / через None» і тисни «Викликати». Стеж за адресами результатів.
р1 is р2 дає False — кожен виклик отримав власний обʼєкт.Як писати правильно
Ліки стандартні й ти їх уже бачив: значенням за замовчуванням ставлять
None, а справжній порожній список створюють усередині
функції — тобто на кожному виклику заново.
None тут ідеальний, бо він незмінний і існує в програмі в єдиному
примірнику: зіпсувати його неможливо в принципі. Порівняння робимо через is,
а не == — це саме той рідкісний випадок, про який казала тема 03.
if not кошик: кошик = [] — коротше ж. Але not спрацює і на
порожньому списку, який тобі передали навмисно. Тоді функція мовчки викине
чужий обʼєкт і почне писати у свій новий, а той, хто передавав, нічого не отримає.
Перевіряй саме is None: питання ж не «чи він порожній», а «чи його
передали».Правило можна сформулювати одним реченням: значенням за замовчуванням може
бути лише незмінний обʼєкт. Числа, рядки, None, True,
кортежі з незмінних елементів — можна. Список, словник, множина — ніколи.
None — теж значення. Іноді
None є законним аргументом і його треба відрізняти від «не передали».
Тоді заводять власний вартовий (sentinel): _НЕМАЄ = object() — унікальний
обʼєкт, який більше ніде не трапиться, і перевіряють if x is _НЕМАЄ. Саме
так влаштовані деякі функції стандартної бібліотеки. Другий бік медалі: пастку іноді
використовують навмисно — як дешевий кеш, що переживає виклики
(def f(n, кеш={})). Це працює, але для цього є чесний інструмент —
functools.lru_cache, і саморобний варіант мовчки ламається, щойно хтось
передасть свій словник.05 / ПередачаЩо саме передається у функцію
Тепер найголовніше непорозуміння всієї теми. Питання звучить так: якщо я передав у функцію список і функція його змінила — чи побачу я зміну ззовні?
У підручниках із інших мов на це відповідають словами «передача за значенням» (копія)
або «передача за посиланням» (оригінал). Python не робить ні того, ні іншого — точніше,
робить єдину річ, яку ти вже знаєш: параметр стає ще одним ярликом на той самий
обʼєкт. Виклик дописати(кошик) усередині діє рівно як присвоєння
список = кошик. Ніякої копії не робиться; ніякого доступу до самого імені
кошик функція не отримує.
Звідси — головний висновок теми, і він у двох частинах:
- Змінити вміст переданого обʼєкта — можна.
список.append(...)редагує обʼєкт у місці, а на нього дивиться і зовнішнє імʼя. Зміна видима ззовні. - Перепризначити імʼя — не можна.
список = [...]перечіпляє локальний ярлик на новий обʼєкт. Зовнішнє імʼя лишається там, де було.
Дві дії виглядають майже однаково й називаються обидві «змінити список», а наслідки протилежні. Пройди обидві колонки по кроках:
Інтерактив 3 · Змінити обʼєкт проти перепризначити імʼя
Ліворуч append, праворуч присвоєння. Аргумент той самий — результат різний.
Ця модель має офіційну назву — передача за спільним використанням (call by sharing). Але назву можна й не запамʼятовувати: досить памʼятати, що аргумент — це ярлик, а не коробка.
додати_в_кошик(кошик, товар) — змінює й повертає
None, або з_доданим(кошик, товар) — нічого не чіпає й повертає
новий список. Так, до речі, влаштована й сама мова:
список.sort() сортує в місці й повертає None, а
sorted(список) повертає новий список.06 / Змінна кількість*args і **kwargs
Досі кількість параметрів була відома наперед. Але буває інакше: print()
приймає скільки завгодно значень, max() — теж. Для цього є дві зірочки.
*рештазбирає всі зайві позиційні аргументи в кортеж. Саме кортеж, а не список — бо це фіксований набір, який функція не має редагувати (тема 07).**опціїзбирає всі зайві іменовані аргументи у словник: ключ — рядок з іменем, значення — переданий обʼєкт.
Зірочка — це синтаксис, а args і kwargs — просто традиційні
імена (від arguments і keyword arguments). Називати можна як завгодно; головне — зірочки
й порядок: спершу звичайні параметри, потім *, потім **.
Покрути повзунки й подивись, що куди потрапляє:
Інтерактив 4 · Куди що збирається
Додавай зайві аргументи — і дивись, як росте кортеж і як росте словник.
назва — він оголошений окремо. Решта позиційних лягає в кортеж у
тому самому порядку, а іменовані — у словник, де порядок відповідає порядку
в виклику. Обидва контейнери порожні (() і {}), якщо
зайвого нічого не передали, — а не None.Коли це справді потрібно
Чесна відповідь: рідше, ніж здається. Є два випадки, де без зірочок ніяк:
- Функція за природою приймає скільки завгодно однорідних значень.
def сума_усіх(*числа)— тут кількість справді не визначена наперед. - Обгортка, яка передає аргументи далі, не знаючи їх. Це основа
декораторів (функція як обʼєкт — тема 14,
самі декоратори — тема 31). Обгортка приймає
*args, **kwargsі віддає їх справжній функції, бо її справа — не аргументи, а те, що робиться до й після.
А от чого робити не варто — писати *args, **kwargs
«про всяк випадок», коли параметри насправді відомі. Ціна такого рішення висока:
- Зникає підказка. Рядок
def оформити(товар, кількість=1, знижка=0.0)— це документація, яку видно і в редакторі, і вhelp(). Рядокdef оформити(*args, **kwargs)не каже нічого. - Зникають хороші помилки. Замість чіткого «missing 1 required positional argument:
'товар'» ти отримаєш
KeyErrorдесь у глибині тіла — на десять рядків нижче й без натяку на причину. - Одруківка в імені перестає ловитися.
оформити("мед", знишка=0.1)у звичайної функції дастьTypeError: got an unexpected keyword argument, а в**kwargsтихо осяде у словнику й буде проігнорована.
07 / РозпакуванняЗірочка з іншого боку: розпакування при виклику
Ті самі зірочки працюють і в зворотний бік — у рядку виклику. Там вони не збирають, а розкладають. Це прямий родич розпакування кортежів із теми 07, тільки замість імен ліворуч — параметри функції.
Одна зірочка розкладає будь-що ітероване (список, кортеж, навіть range) на
позиційні аргументи — по порядку. Дві зірочки розкладають словник на
іменовані аргументи: ключі стають іменами параметрів, тому вони мають
бути рядками й збігатися з іменами в def.
Найважливіше тут — не плутати зірочку з розпакуванням списку в один аргумент. Різниця істотна:
| виклик | що отримає функція | коли це те, що треба |
|---|---|---|
| f(позиція) | один аргумент — сам список | функція справді чекає список |
| f(*позиція) | три окремі аргументи | у функції три параметри |
| f(**налаштування) | іменовані аргументи за ключами | ключі збігаються з іменами параметрів |
Помилки тут теж говіркі. Якщо в списку більше елементів, ніж параметрів, буде
TypeError: оформити() takes from 1 to 3 positional arguments but 4 were given.
Якщо у словнику зайвий ключ — TypeError: got an unexpected keyword argument 'вага'.
І в обох випадках повідомлення прямо називає, що не зійшлося.
функція(**конфіг) замінює довгий ручний перелік. Але не захоплюйся:
якщо аргументи відомі, звичайний явний виклик читається краще.08 / ОбмеженняТільки позиційні й тільки іменовані
Для повноти — два рідші, але корисні позначки в списку параметрів. Обидві не додають нових можливостей, а відбирають їх у того, хто викликає, і саме тому корисні.
- Скісна риска
/означає: усе, що стоїть лівіше, можна передати лише позиційно. Викликоформити(товар="мед")після цього дастьTypeError. Навіщо? Щоб імʼя параметра лишалося внутрішньою справою функції — тоді його можна перейменувати, не ламаючи чужий код. Так оголошені майже всі вбудовані функції: уhelp(len)ти побачиш самеlen(obj, /). - Зірочка
*означає: усе, що стоїть правіше, можна передати лише на імʼя. Викликоформити("мед", 3, 0.1)стане помилкою — знижку доведеться писати якзнижка=0.1. Це той самий прийом читабельності з розділу 02, але вже примусовий: автор функції не радить іменувати прапорці, а вимагає.
Практична порада для новачка: писати / тобі поки не знадобиться, а от
* перед булевими налаштуваннями — дуже пристойний тон. Якщо у функції більше
двох-трьох параметрів і серед них є True/False, зроби їх тільки-іменованими,
і виклики самі стануть читабельними.
09 / LEGBДе Python шукає імʼя
Друга половина теми. Функція має власну памʼять: імена, звʼязані всередині неї, зникають разом із виходом із функції. Але всередині можна звертатися й до імен, оголошених ззовні, — і питання в тому, у якому порядку Python їх шукає.
Порядок фіксований і має мнемоніку LEGB — чотири букви за першими літерами англійських назв. Пошук іде зсередини назовні й зупиняється на першому ж рівні, де імʼя знайшлося:
| рівень | що це | приклад імені |
|---|---|---|
| L · Local | локальна памʼять поточного виклику: параметри й усе, чому присвоювали в тілі | кошик усередині додати |
| E · Enclosing | памʼять зовнішньої функції, якщо ця вкладена в іншу | лічильник у функції-обгортці |
| G · Global | рівень модуля — усе, що оголошено у файлі поза функціями | наш словник ЦІНИ |
| B · Built-in | вбудовані імена, які є завжди й ніде не оголошені | len, sum, print |
Головне слово тут — перший. Python не збирає всі варіанти й не обирає кращий: він бере той, що трапився раніше, і далі не дивиться. Тому локальне імʼя завжди перекриває глобальне, а глобальне — вбудоване. Постав галочки й подивись, де саме зупиняється пошук:
Інтерактив 5 · Чотири рівні пошуку імені
Обери імʼя й познач, на яких рівнях воно оголошене. Стрілка ліворуч — шлях пошуку.
sum пошук дійде до
вбудованих і щось знайде — тому помилки не буде ніколи. Для підсумок
вбудованого варіанта не існує, пошук дійде до кінця ні з чим і дасть
NameError. Це буквально та сама помилка, що й від одруківки в імені.Два уточнення, які варто зафіксувати одразу.
Рівень E — це не «будь-яка зовнішня функція». Це саме функція, у тіло
якої вкладено нашу, у тексті програми. Якщо а() викликає б(), то
б() не бачить локальних імен а() — вони не вкладені одна
в одну, а лише йдуть одна за одною в часі. Область видимості визначається
текстом, а не ланцюжком викликів. Цю властивість називають
лексичною областю видимості.
Локальність визначається наперед. Python вирішує, які імена у функції локальні, ще під час компіляції — за одним простим правилом: якщо десь у тілі є присвоєння цьому імені, воно локальне для всього тіла, від першого рядка. Не з місця присвоєння, а від початку. Саме звідси береться помилка, яку ми зараз побачимо.
10 / Записуванняglobal і nonlocal
Читати зовнішнє імʼя з функції можна вільно — це просто крок G або E у пошуку. А от присвоїти йому — інша справа. Спробуємо порахувати виклики:
Виглядає бездоганно, а падає:
Читається так: «неможливо звернутися до локальної змінної, якій ще нічого не
присвоєно». Чому локальної? Бо в тілі є рядок лічильник = ... — і за правилом
з попереднього розділу це робить імʼя локальним для всієї функції. Праворуч від
= Python шукає лічильник уже тільки в локальній памʼяті,
знаходить його там оголошеним, але ще ні на що не звʼязаним — і зупиняється.
Щоб сказати «я маю на увазі саме зовнішнє імʼя», існують дві заяви:
global імʼя— «присвоєння цьому імені йде на рівень модуля (G)».nonlocal імʼя— «присвоєння йде на найближчий рівень обгортки (E)». Працює тільки всередині вкладеної функції й тільки якщо таке імʼя в обгортці справді є.
Інтерактив 6 · Читання, запис і дві заяви
Пʼять майже однакових функцій. Дивись, що стало з памʼяттю й що надрукувалось.
append. Присвоєння створює локальне імʼя, виклик метода —
ні. Це та сама пара з інтерактиву 3, тільки тепер видно з боку памʼяті.Чому їх майже завжди варто уникати
global — не помилка мови й не «поганий стиль» через моду. Проблема
конкретна: функція з global перестає бути функцією в
корисному сенсі. Її результат більше не визначається аргументами, а її виклик тепер має
побічний ефект, невидимий у рядку виклику.
- Її не можна перевірити окремо. Щоб протестувати таку функцію, треба спершу привести глобальний стан у потрібний вигляд, а після — прибрати за собою.
- Її не можна прочитати локально. Дивлячись на
додати(), ти не знаєш, що змінилося, — треба шукати всіglobalу тілі. - Порядок викликів починає значити. Два виклики поспіль дають не те саме, що один, і код стає чутливим до речей, які раніше були неважливими.
Заміна майже завжди одна й та сама: прийми стан аргументом і поверни новий стан.
Стало на один рядок довше — і на цілий клас багів менше. Коли станів багато, їх збирають у словник або в обʼєкт класу (тема 25), і тоді функція змінює саме його, а не все підряд.
nonlocal і замикання. Вкладена функція,
яка користується іменами обгортки, разом із цими іменами утворює замикання
(closure). Обгортка вже завершилась, а її памʼять живе, поки жива вкладена функція, — саме
на цьому побудовані декоратори. Побачити захоплені імена можна:
внутрішня.__code__.co_freevars покаже їх кортежем. Ключова відмінність від
global: nonlocal тримає стан у маленькому, локальному й приватному
закутку, а не в спільному просторі модуля, — тому за нього критикують значно менше. Докладно
все це розберемо в темі 31.11 / ЗатіненняЗатінення вбудованих імен
Останній наслідок правила LEGB — і той, на якому найчастіше спотикаються новачки.
Оскільки глобальний рівень перевіряється раніше за вбудований, будь-яке своє імʼя
перекриває однойменну вбудовану функцію. Помилки при цьому немає: рядок
sum = 0 абсолютно законний.
Повідомлення спантеличує: чому раптом «обʼєкт типу int неможливо викликати», якщо
я викликаю sum? Бо sum тепер справді нуль. Вбудована функція
нікуди не поділася — просто дістатися до неї по імені більше не вдається: пошук
зупиняється на рівні G. Пройди по кроках:
Інтерактив 7 · Як зникає вбудована функція
Обери імʼя й тисни «Наступний крок». Дивись, на якому рівні зупиняється пошук.
del sum прибирає ярлик із
глобального рівня, пошук іде далі й знову знаходить вбудовану функцію. У справжньому
коді, звісно, роблять не так — просто дають своїй змінній інше імʼя.Найнебезпечніші імена — короткі й привабливі саме як назви для власних даних:
list, dict, set, str, type,
id, sum, min, max, next,
input, file, hash. Особливо підступний
id: якщо його затінити числом, помилка вилізе не там, де ти писав, а через
двісті рядків, у чужій функції, яка чесно кличе id().
Конвенція така: якщо потрібне саме таке слово, додай підкреслення в кінці —
list_, id_. Це офіційна рекомендація стандарту оформлення коду.
Але частіше знайдеться просто краще імʼя: не list, а покупки;
не sum, а до_сплати.
list, вбудована функція зникне тільки в межах цієї
функції. Знайти таке важче: той самий код працює в одному місці й падає в іншому. Редактори
зазвичай підсвічують такі імена — не ігноруй попередження.12 / ПідсумокЩо забрати з теми
Тема виглядала як дві різні: аргументи й видимість. Насправді обидві половини тримаються на одному й тому самому реченні з теми 03 — імʼя вказує на обʼєкт.
- Позиційно вирішує порядок, іменовано — імʼя. Позиційні завжди
стоять першими. Прапорці й числа-налаштування іменуй завжди: виклик має читатися
без походу в
def. - Значення за замовчуванням створюється один раз, у момент виконання
рядка
def, і живе вфункція.__defaults__. Тому змінюваний обʼєкт там ставити не можна ніколи — правильно черезNoneі перевіркуis None. - Аргумент — це ярлик на той самий обʼєкт. Змінити його вміст можна, і це видно ззовні; перепризначити імʼя всередині функції — можна, але ззовні цього не видно.
*argsі**kwargsзбирають зайве в кортеж і словник, а ті самі зірочки при виклику — навпаки, розкладають готовий список чи словник по параметрах. Брати їх варто лише тоді, коли кількість аргументів справді невідома.- LEGB — порядок пошуку імені: локальна, обгортка, глобальна,
вбудовані. Перший знайдений виграє, решта не розглядається. Не знайшли ніде —
NameError. - Присвоєння в тілі робить імʼя локальним для всього тіла. Звідси
UnboundLocalError; звідси ж потреба вglobalіnonlocal— і причина, чому краще передати стан аргументом і повернути результат.
Далі — тема 16: модулі й пакети.
Ми щойно розібралися, як імена живуть усередині одного файла. Питання наступної теми
природне: а що коли файлів кілька? Виявиться, що import — це теж просто
звʼязування імені, а рівень G, який ми звали «глобальним», насправді глобальний лише
в межах одного модуля.
practice.ipynb ти впіймаєш пастку
змінюваного замовчування власними руками — з assert, який доводить, що список
справді накопичується між викликами, — перевіриш __defaults__ до й після,
зловиш UnboundLocalError, побачиш усі чотири рівні LEGB на живих прикладах і
навчишся розпаковувати аргументи з готового словника.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.