Python з нуля · Блок 3 · Тема 15

Хто кого бачить

Функція вміє брати дані ззовні й має власний закуток памʼяті. Половина найдивніших багів Python народжується рівно там, де ці дві речі перетинаються.

У темі 14 ми навчилися давати шматку коду імʼя: написали def, перелічили параметри, повернули результат через return. Функція запрацювала — і на цьому більшість підручників тему закриває. Але між рядком виклику й рядком def насправді відбувається доволі багато: Python має вирішити, який аргумент якому параметру дістанеться, звідки взяти те, чого не передали, і де шукати імена, яких у функції немає взагалі. Поки все просто, ці рішення непомітні. Щойно зʼявляється список за замовчуванням або спроба змінити зовнішню змінну — вони стають головними.

Ця тема — про два механізми: як виклик лягає на параметри і де функція шукає імена. Обидва повністю пояснюються моделлю ярликів із теми 03, тож нічого принципово нового вчити не доведеться — доведеться довчити наслідки.

01 / ДомовленістьВиклик — це домовленість

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

ЦІНИ = {"хліб": 28.5, "молоко": 32.0, "яблука": 19.9, "мед": 145.0, "сіль": 12.0} def оформити(товар, кількість=1, знижка=0.0): """Скільки коштує позиція кошика після знижки.""" сума = ЦІНИ[товар] * кількість return round(сума * (1 - знижка), 2)

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

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

Чому це важливо. Помилка «не так передали аргументи» — TypeError, і вона зупиняє програму до входу у функцію: жодної напівроботи, жодних зіпсованих даних. Повідомлення майже завжди прямо називає проблемний параметр.

02 / РозподілПозиційні й іменовані аргументи

Передати аргумент можна двома способами, і вони вільно змішуються в одному виклику.

Позиційно — просто значення, без імені: оформити("мед", 3). Тут усе вирішує порядок: перший аргумент дістається першому параметру, другий — другому. Переставиш місцями — Python не помітить підміни й порахує три меди по ціні… ні, він спробує знайти в словнику ключ 3 і впаде з KeyError. Це ще щасливий випадок: коли обидва аргументи числа, помилка мовчки дасть неправильну відповідь.

Іменовано (keyword argument) — з явною вказівкою параметра: оформити(товар="мед", кількість=3). Тут порядок не має значення взагалі: оформити(кількість=3, товар="мед") — це той самий виклик. Python розкладає аргументи по іменах, а не по місцях.

Правило змішування одне й воно жорстке: усі позиційні аргументи стоять перед усіма іменованими. Інакше Python навіть не почне виконувати файл — це SyntaxError, помилка розбору, а не виконання.

Інтерактив 1 · Як виклик лягає на параметри

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

передано1
за замовч.2
повертає145.0
Що видно: помаранчева стрілка — аргумент, переданий за позицією, бірюзова — за іменем. Пунктирна рамка означає, що значення взялося за замовчуванням, бо його не передали. Три останні варіанти — три різні способи помилитися, і кожен дає своє повідомлення.

Коли варто писати імена, а коли зайве? Орієнтир такий: перший аргумент майже завжди зрозумілий із назви функції, а третій — майже ніколи. Порівняй два рядки, які роблять одне й те саме:

нарахувати(1000, 0.05, True, False) нарахувати(1000, ставка=0.05, щомісяця=True, капіталізація=False)

Перший рядок неможливо прочитати без документації. Що таке True? А False? Другий читається як речення. Витрати — двадцять зайвих символів, зиск — той, хто читатиме код через півроку (найімовірніше, ти сам), не полізе шукати def. Практичне правило: булеві прапорці й числа-налаштування завжди іменуй, а те, з чим функція працює по суті, можна лишити позиційним.

03 / ЗамовчуванняЗначення за замовчуванням

Параметр із = у рядку def має значення за замовчуванням (default value). Це відповідь на питання «а що робити, якщо цього аргументу не передали?». Функція оформити має два таких параметри: кількість=1 і знижка=0.0 — розумні припущення, які влаштовують більшість викликів.

Тут діє одне синтаксичне обмеження: параметри без замовчування йдуть першими. Інакше Python не зміг би розкласти позиційні аргументи однозначно, тому забороняє такий запис одразу:

>>> def оформити(кількість=1, товар): SyntaxError: parameter without a default follows parameter with a default

Читається дослівно: «параметр без замовчування йде після параметра із замовчуванням». Виправлення очевидне — поміняти їх місцями.

Значення за замовчуванням — не абстракція, його можна побачити. Кожна функція в Python — теж обʼєкт (памʼятаєш, у темі 03 ми казали, що обʼєктом є усе?), і в неї є атрибут __defaults__: кортеж уже готових обʼєктів.

>>> оформити.__defaults__ (1, 0.0)

Зверни увагу на слово «уже готових». Кортеж існує до першого виклику. Це не опис «що створювати», це самі обʼєкти, які лежать у функції й чекають. Саме цей факт зараз усе й пояснить.

04 / ПасткаЗмінюване значення за замовчуванням

Ми коротко зачепили цю пастку в темі 03. Тепер розберемо її до дна: це найвідоміша пастка мови, і питання про неї трапляється на співбесідах саме тому, що відповідь показує, чи розуміє людина модель обʼєктів.

Задача побутова: функція додає товар у кошик, а якщо кошика немає — заводить новий порожній. Запис проситься сам:

def додати(товар, кошик=[]): кошик.append(товар) return кошик

Читається бездоганно: «кошик за замовчуванням порожній». Перевіримо:

>>> додати("яблуко") ['яблуко'] >>> додати("груша") ['яблуко', 'груша'] >>> додати("слива") ['яблуко', 'груша', 'слива']

Другий виклик повернув два елементи, третій — три. Функція памʼятає все, що їй колись давали, хоча ми ніде не просили її нічого запамʼятовувати.

Чому так

Розберемо по кроках, і кожен крок — уже знайома тобі річ.

крок 1 · def — це теж виконуваний рядок

Рядок def не «описує» функцію для компілятора. Він виконується: Python створює обʼєкт-функцію й чіпляє на нього імʼя додати — точно так само, як x = 5 створює число й чіпляє імʼя.

крок 2 · вирази замовчувань обчислюються тут-таки

Щоб створити обʼєкт-функцію, Python має обчислити всі значення за замовчуванням негайно — і скласти з них кортеж __defaults__. Отже, порожній список [] створюється один-єдиний раз, у момент читання def.

додати.__defaults__  →  ([],)
крок 3 · виклик нічого не створює наново

Коли аргумент кошик не передали, Python бере готовий обʼєкт із __defaults__ і чіпляє на нього локальне імʼя кошик. Це звичайне звʼязування імені — жодного копіювання. Список у функції й список у __defaults__той самий обʼєкт.

крок 4 · append змінює обʼєкт у місці

А тепер згадай поділ із теми 03: append не створює нового списку, він редагує наявний. Тобто дописує просто в той обʼєкт, що лежить у __defaults__ і чекатиме там наступного виклику.

після трьох викликів: додати.__defaults__ → (['яблуко', 'груша', 'слива'],)

Ніякої магії. Одне звичайне правило («def виконується один раз») плюс друге звичайне правило («змінюваний обʼєкт редагується в місці») дають разом поведінку, якої ніхто не замовляв. Натисни «Викликати» кілька разів і подивись на адреси:

Інтерактив 2 · Список за замовчуванням живе між викликами

Перемикай «пастка / через None» і тисни «Викликати». Стеж за адресами результатів.

викликів0
списків1
р1 is р2
len(остан.)
Головне — адреси. У режимі «пастка» всі результати мають однакову адресу: це один список, який росте. У режимі «через None» адреси різні, і р1 is р2 дає False — кожен виклик отримав власний обʼєкт.

Як писати правильно

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

def додати(товар, кошик=None): if кошик is None: # аргумент не передали — заводимо свій кошик = [] кошик.append(товар) return кошик

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 не робить ні того, ні іншого — точніше, робить єдину річ, яку ти вже знаєш: параметр стає ще одним ярликом на той самий обʼєкт. Виклик дописати(кошик) усередині діє рівно як присвоєння список = кошик. Ніякої копії не робиться; ніякого доступу до самого імені кошик функція не отримує.

Звідси — головний висновок теми, і він у двох частинах:

Дві дії виглядають майже однаково й називаються обидві «змінити список», а наслідки протилежні. Пройди обидві колонки по кроках:

Інтерактив 3 · Змінити обʼєкт проти перепризначити імʼя

Ліворуч append, праворуч присвоєння. Аргумент той самий — результат різний.

кошик зліва
кошик справа
обʼєктів зліва1
обʼєктів справа1
Ключ: на кроці 3 ліворуч стрілки не рухаються — змінюється сам обʼєкт, і це видно ззовні. Праворуч зʼявляється другий обʼєкт, локальне імʼя перечіпляється на нього, а зовнішній кошик лишається старим. На кроці 4 локальні імена зникають — і новий обʼєкт помирає, так нікуди й не потрапивши.

Ця модель має офіційну назву — передача за спільним використанням (call by sharing). Але назву можна й не запамʼятовувати: досить памʼятати, що аргумент — це ярлик, а не коробка.

Практичний висновок. Функція, яка мовчки змінює передані їй дані, — джерело найважчих багів, бо ефект проявляється деінде. Обирай одне з двох і пиши про це в назві: або додати_в_кошик(кошик, товар) — змінює й повертає None, або з_доданим(кошик, товар) — нічого не чіпає й повертає новий список. Так, до речі, влаштована й сама мова: список.sort() сортує в місці й повертає None, а sorted(список) повертає новий список.

06 / Змінна кількість*args і **kwargs

Досі кількість параметрів була відома наперед. Але буває інакше: print() приймає скільки завгодно значень, max() — теж. Для цього є дві зірочки.

Зірочка — це синтаксис, а args і kwargs — просто традиційні імена (від arguments і keyword arguments). Називати можна як завгодно; головне — зірочки й порядок: спершу звичайні параметри, потім *, потім **. Покрути повзунки й подивись, що куди потрапляє:

Інтерактив 4 · Куди що збирається

Додавай зайві аргументи — і дивись, як росте кортеж і як росте словник.

len(решта)2
len(опції)1
Що видно: перший аргумент завжди йде в звичайний параметр назва — він оголошений окремо. Решта позиційних лягає в кортеж у тому самому порядку, а іменовані — у словник, де порядок відповідає порядку в виклику. Обидва контейнери порожні (() і {}), якщо зайвого нічого не передали, — а не None.

Коли це справді потрібно

Чесна відповідь: рідше, ніж здається. Є два випадки, де без зірочок ніяк:

  1. Функція за природою приймає скільки завгодно однорідних значень. def сума_усіх(*числа) — тут кількість справді не визначена наперед.
  2. Обгортка, яка передає аргументи далі, не знаючи їх. Це основа декораторів (функція як обʼєкт — тема 14, самі декоратори — тема 31). Обгортка приймає *args, **kwargs і віддає їх справжній функції, бо її справа — не аргументи, а те, що робиться до й після.

А от чого робити не варто — писати *args, **kwargs «про всяк випадок», коли параметри насправді відомі. Ціна такого рішення висока:

07 / РозпакуванняЗірочка з іншого боку: розпакування при виклику

Ті самі зірочки працюють і в зворотний бік — у рядку виклику. Там вони не збирають, а розкладають. Це прямий родич розпакування кортежів із теми 07, тільки замість імен ліворуч — параметри функції.

позиція = ["мед", 3, 0.1] оформити(*позиція) # те саме, що оформити("мед", 3, 0.1) налаштування = {"кількість": 3, "знижка": 0.1} оформити("мед", **налаштування) # те саме, що оформити("мед", кількість=3, знижка=0.1)

Одна зірочка розкладає будь-що ітероване (список, кортеж, навіть range) на позиційні аргументи — по порядку. Дві зірочки розкладають словник на іменовані аргументи: ключі стають іменами параметрів, тому вони мають бути рядками й збігатися з іменами в def.

Найважливіше тут — не плутати зірочку з розпакуванням списку в один аргумент. Різниця істотна:

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

Помилки тут теж говіркі. Якщо в списку більше елементів, ніж параметрів, буде TypeError: оформити() takes from 1 to 3 positional arguments but 4 were given. Якщо у словнику зайвий ключ — TypeError: got an unexpected keyword argument 'вага'. І в обох випадках повідомлення прямо називає, що не зійшлося.

Де це трапляється щодня. Найчастіше — коли аргументи зібрані десь окремо: прочитані з файла налаштувань, порахувані циклом, повернуті іншою функцією. Тоді функція(**конфіг) замінює довгий ручний перелік. Але не захоплюйся: якщо аргументи відомі, звичайний явний виклик читається краще.

08 / ОбмеженняТільки позиційні й тільки іменовані

Для повноти — два рідші, але корисні позначки в списку параметрів. Обидві не додають нових можливостей, а відбирають їх у того, хто викликає, і саме тому корисні.

def оформити(товар, /, кількість=1, *, знижка=0.0): ...

Практична порада для новачка: писати / тобі поки не знадобиться, а от * перед булевими налаштуваннями — дуже пристойний тон. Якщо у функції більше двох-трьох параметрів і серед них є 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 у пошуку. А от присвоїти йому — інша справа. Спробуємо порахувати виклики:

лічильник = 0 def додати(): лічильник = лічильник + 1 додати()

Виглядає бездоганно, а падає:

UnboundLocalError: cannot access local variable 'лічильник' where it is not associated with a value

Читається так: «неможливо звернутися до локальної змінної, якій ще нічого не присвоєно». Чому локальної? Бо в тілі є рядок лічильник = ... — і за правилом з попереднього розділу це робить імʼя локальним для всієї функції. Праворуч від = Python шукає лічильник уже тільки в локальній памʼяті, знаходить його там оголошеним, але ще ні на що не звʼязаним — і зупиняється.

Щоб сказати «я маю на увазі саме зовнішнє імʼя», існують дві заяви:

Інтерактив 6 · Читання, запис і дві заяви

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

результат
Порівняй перший і пʼятий варіанти. В обох немає жодної заяви — і обидва працюють. Різниця в тому, що п'ятий не присвоює імені, а змінює обʼєкт у місці через append. Присвоєння створює локальне імʼя, виклик метода — ні. Це та сама пара з інтерактиву 3, тільки тепер видно з боку памʼяті.

Чому їх майже завжди варто уникати

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

Заміна майже завжди одна й та сама: прийми стан аргументом і поверни новий стан.

def додати(лічильник): # стан приходить явно return лічильник + 1 # і йде назад явно лічильник = додати(лічильник)

Стало на один рядок довше — і на цілий клас багів менше. Коли станів багато, їх збирають у словник або в обʼєкт класу (тема 25), і тоді функція змінює саме його, а не все підряд.

Для профі: nonlocal і замикання. Вкладена функція, яка користується іменами обгортки, разом із цими іменами утворює замикання (closure). Обгортка вже завершилась, а її памʼять живе, поки жива вкладена функція, — саме на цьому побудовані декоратори. Побачити захоплені імена можна: внутрішня.__code__.co_freevars покаже їх кортежем. Ключова відмінність від global: nonlocal тримає стан у маленькому, локальному й приватному закутку, а не в спільному просторі модуля, — тому за нього критикують значно менше. Докладно все це розберемо в темі 31.

11 / ЗатіненняЗатінення вбудованих імен

Останній наслідок правила LEGB — і той, на якому найчастіше спотикаються новачки. Оскільки глобальний рівень перевіряється раніше за вбудований, будь-яке своє імʼя перекриває однойменну вбудовану функцію. Помилки при цьому немає: рядок sum = 0 абсолютно законний.

>>> sum = 0 # цілком дозволений рядок >>> sum([1, 2, 3]) TypeError: 'int' object is not callable

Повідомлення спантеличує: чому раптом «обʼєкт типу int неможливо викликати», якщо я викликаю sum? Бо sum тепер справді нуль. Вбудована функція нікуди не поділася — просто дістатися до неї по імені більше не вдається: пошук зупиняється на рівні G. Пройди по кроках:

Інтерактив 7 · Як зникає вбудована функція

Обери імʼя й тисни «Наступний крок». Дивись, на якому рівні зупиняється пошук.

пошук зупинився на
Крок 4 показує ліки: del sum прибирає ярлик із глобального рівня, пошук іде далі й знову знаходить вбудовану функцію. У справжньому коді, звісно, роблять не так — просто дають своїй змінній інше імʼя.

Найнебезпечніші імена — короткі й привабливі саме як назви для власних даних: list, dict, set, str, type, id, sum, min, max, next, input, file, hash. Особливо підступний id: якщо його затінити числом, помилка вилізе не там, де ти писав, а через двісті рядків, у чужій функції, яка чесно кличе id().

Конвенція така: якщо потрібне саме таке слово, додай підкреслення в кінці — list_, id_. Це офіційна рекомендація стандарту оформлення коду. Але частіше знайдеться просто краще імʼя: не list, а покупки; не sum, а до_сплати.

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

12 / ПідсумокЩо забрати з теми

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

Далі — тема 16: модулі й пакети. Ми щойно розібралися, як імена живуть усередині одного файла. Питання наступної теми природне: а що коли файлів кілька? Виявиться, що import — це теж просто звʼязування імені, а рівень G, який ми звали «глобальним», насправді глобальний лише в межах одного модуля.

Далі в практиці. У practice.ipynb ти впіймаєш пастку змінюваного замовчування власними руками — з assert, який доводить, що список справді накопичується між викликами, — перевіриш __defaults__ до й після, зловиш UnboundLocalError, побачиш усі чотири рівні LEGB на живих прикладах і навчишся розпаковувати аргументи з готового словника.

Далі в темі

Теорію прочитано. Тепер закріпи її на практиці.