Тринадцять тем ми складали інструменти: імена, числа, рядки, списки, словники, умови, цикли, спискові включення. Кожна програма з них виходила однаковою за формою — довгий стовпчик рядків, який інтерпретатор проходить один раз від початку до кінця. Якщо тобі треба зробити те саме ще раз для інших даних, лишалось одне: скопіювати шматок і підправити в копії числа.
Ця тема — поворотна. Вона не додає нового типу даних і не вчить нової дії над ними. Вона додає спосіб організувати те, що вже є: узяти шматок коду, дати йому імʼя й потім згадувати цим імʼям замість того, щоб переписувати. Далі весь курс буде складатися саме з функцій — тому цю тему варто засвоїти повністю, а не «в загальних рисах».
Наскрізний приклад лишається тим самим кошиком покупок, з яким ми працювали в темі 06 і темі 12:
01 / МотиваціяНавіщо взагалі функції
Уяви, що для кожної позиції в чеку треба порахувати вартість зі знижкою 10%. Без функцій це виглядає так: три рядки на позицію, і так стільки разів, скільки позицій.
Код працює. Проблема в іншому: формула записана кілька разів. Завтра
знижку змінять з 10% на 12% — і треба знайти всі місця, де стоїть 0.9, і
виправити кожне. Пропустиш одне — програма й далі рахуватиме, просто інколи неправильно.
Це найгірший сорт помилки: без падіння, без повідомлення, з тихо зіпсованими числами.
Функція вирішує це прямо: формула існує в одному місці, а всі інші рядки на неї посилаються. Покрути повзунок і подивись, як розходяться дві версії того самого коду, коли позицій стає більше.
Інтерактив 1 · До і після рефакторингу
Ліворуч — повторений код, праворуч — функція й виклики. Рожевим позначений рядок із формулою знижки.
Отже, три причини, і вони різні за вагою:
- Одне джерело правди. Правило записане один раз. Виправлення теж робиться один раз — і одразу діє скрізь.
- Імʼя для наміру. Рядок
підсумок(28.5, 2)читається як речення. Три рядки арифметики поспіль читаються як арифметика: щоб зрозуміти навіщо вони, доведеться їх розібрати. - Можливість перевірити окремо. Функцію можна викликати з контрольними даними й порівняти результат із відомою відповіддю. Шматок коду посеред скрипту так не перевіриш — доведеться запускати весь скрипт.
02 / ВизначенняЯк влаштований def
Функція створюється ключовим словом def (від define — визначити).
Ось та сама формула, оформлена як функція:
Розберімо рядок за рядком:
def— слово, з якого починається визначення.підсумок— імʼя функції. Правила ті самі, що для будь-якого імені з теми 03: літери, цифри, підкреслення, не з цифри. За домовленістю — маленькими літерами через підкреслення, і бажано дієсловом або тим, що функція повертає.(ціна, кількість)— параметри в дужках через кому. Це імена, які зʼявляться всередині функції, коли її викличуть.:— двокрапка, після якої йде тіло: рядки з відступом. Відступ тут працює точно так само, як в умовах і циклах — саме він показує, де тіло починається й де закінчується.return— про нього окремий розділ нижче.
Дужки в цьому рядку порожніми бути можуть: def привітати(): — цілком
законна функція без параметрів. А от самі дужки обовʼязкові завжди, навіть порожні.
Визначення не виконує тіло
Тепер найважливіше в цьому розділі, і саме тут новачки спотикаються найчастіше.
Коли інтерпретатор доходить до рядка def, він не виконує тіло
функції. Він робить дві речі: створює в памʼяті обʼєкт-функцію (усередині
якого лежить готовий до виконання код тіла) і чіпляє на цей обʼєкт ярлик із назвою
функції. Усе. Тіло не запускається жодного разу.
Тіло виконується тільки тоді, коли функцію викликають — тобто
пишуть її імʼя з дужками: підсумок(28.5, 2). Дужки тут і є командою
«виконай». Пройди п'ять кроків і подивись, коли саме щось потрапляє у вивід.
Інтерактив 2 · def нічого не друкує
Крок за кроком. Стеж за третім рядком коду — він усередині функції.
def уже виконано,
імʼя привітати вже існує, а тіло — ні: у виводі досі один рядок.
Виконавець перескочив із другого рядка одразу на четвертий. Тіло дочекається
свого лише на кроці 5, коли до нього прийдуть через виклик.1 / 0 чи звернення до неіснуючого імені спокійно
пролежать у тілі скільки завгодно довго й вистрелять під час першого виклику. Тому
«файл запустився без помилок» ще нічого не означає про функції, які в ньому лежать.03 / ДаніПараметри й аргументи
Два слова, які вживають як синоніми, а насправді вони позначають різні речі — і плутанина тут заважає читати повідомлення про помилки.
- Параметр (parameter) — імʼя в дужках у рядку
def. Це «порожнє місце», підпис на вході. У нашій функції параметри —цінаікількість. - Аргумент (argument) — конкретне значення, яке підставляють при
виклику. У
підсумок(28.5, 2)аргументи —28.5і2.
Коротко: параметр — це те, що написано в оголошенні; аргумент — те, що прилетіло при виклику. Параметр один на всю функцію, аргументи різні на кожен виклик.
За замовчуванням аргументи розподіляються за позицією: перший
аргумент потрапляє в перший параметр, другий — у другий. Тому порядок має значення,
і підсумок(2, 28.5) — це не те саме, що підсумок(28.5, 2).
Тут, до речі, обидва варіанти дадуть однакове число, бо множення комутативне; у
загальному випадку переставлені аргументи дають тихо неправильну відповідь.
Якщо аргументів дати не стільки, скільки параметрів, Python зупинить програму:
Читається буквально: «бракує одного обовʼязкового позиційного аргументу —
кількість». Слово positional у повідомленні — це якраз про
розподіл за позицією.
підсумок(кількість=2, ціна=28.5)), задавати їм значення за замовчуванням,
приймати їх довільну кількість. Усе це — тема 15, разом із питанням,
що станеться зі змінною, створеною всередині функції. Поки що нам вистачить
найпростішого випадку: скільки параметрів оголошено, стільки аргументів і передаємо,
у тому самому порядку.04 / Поверненняreturn: значення й негайний вихід
Функція, яка щось порахувала, має віддати результат назад — туди, звідки її
викликали. Це і робить return. Він робить одразу дві речі, і друга
для новачків несподівана:
- Обчислює вираз праворуч від себе й віддає отримане значення на
місце виклику. Рядок
ціна_зі_знижкою = підсумок(28.5, 2)перетворюється націна_зі_знижкою = 51.3. - Негайно завершує функцію. Не «наприкінці», не «коли дійде до кінця тіла» — просто в цю мить. Усе, що написано в тілі нижче, не виконається ніколи.
Друга властивість дозволяє класти return у різні гілки
умови. Функція розміру знижки — гарний
приклад: правил три, і кожне закінчується своїм поверненням.
Зверни увагу: жодного else тут немає — і він не потрібен. Якщо перший
return спрацював, до другої перевірки виконання просто не дійде. Пересунь
повзунок і подивись, який саме return спрацьовує й скільки рядків
лишається невиконаними.
Інтерактив 3 · return виходить одразу
Змінюй суму. Бірюзові рядки виконано, рожевий — той, що завершив функцію, сірі не виконались.
if. Інші пропущено з іншої причини:
функція вже закінчилась на return, і виконання до них
фізично не дійшло.return.
Якщо жодна гілка не спрацювала, а внизу немає «останнього» return, функція
все одно щось поверне — і це буде None. Прибери в прикладі вище рядок
return 0.10, і виклик знижка(700) тихо віддасть
None замість числа. Помилка вилізе не тут, а рядків через десять, коли
хтось спробує це None помножити.05 / Головна плутанинаreturn проти print
А тепер — розділ, через який новачки втрачають найбільше часу. Дві функції нижче виглядають майже однаково, і на екрані від них наче однаковий ефект. Насправді вони роблять принципово різні речі.
Різниця в тому, кому призначений результат:
returnвіддає значення програмі. Його можна записати в імʼя, покласти в список, передати в іншу функцію, помножити. На екрані при цьому не зʼявляється нічого.printмалює текст на екрані, для людини. Програма цього тексту не отримує — він уже пішов у вивід і назад не повернеться.
І тепер головне. Функція сума_B не має return — отже, вона
повертає None. Рядок y = сума_B(28.5, 2) надрукує 57.0 на
екран і покладе в y нічого. Точніше — None. Натискай
«наступний крок» і дивись на два стовпчики паралельно.
Інтерактив 4 · Повернути чи надрукувати
Дві майже однакові функції. Однакові дії — різна доля результату.
x * 2 дає 114.0, бо в
x лежить число. y * 2 падає з TypeError,
бо в y лежить None — а множити None
не можна. Число, яке ти бачив на екрані, програмі так і не дісталось.None — це не «нічого»
Функція без return повертає None. Це не «порожнеча» і не
«відсутність результату» — це цілком конкретний обʼєкт, єдиний свого роду, з власним
типом NoneType. У нього просто немає корисного вмісту: він означає «тут
значення не передбачене».
Зверни увагу на останній рядок: перевіряти на None прийнято через
is, а не ==. Причину ми розібрали в
темі 03: None існує
в програмі в єдиному примірнику, тому питання «це той самий обʼєкт» тут точніше за
питання «однакового вмісту».
Правило, яке рятує від половини цієї плутанини: функція рахує й повертає,
а друкує той, хто її викликав. Тобто пиши print(підсумок(28.5, 2)),
а не ховай print усередину. Тоді ту саму функцію можна використати і щоб
показати число людині, і щоб додати його до загальної суми — а функція з
print усередині годиться тільки на перше.
06 / МеханікаЩо відбувається під час виклику
Щоб функція могла викликати іншу функцію, а та — третю, і щоб усі вони потім повернулись рівно туди, звідки їх покликали, інтерпретатор веде стек викликів (call stack). Стек — це стопка: класти й брати можна тільки згори.
На кожен виклик Python створює кадр (frame) — коробочку, у якій
живуть параметри цієї функції та її локальні імена. Кадр кладеться на верх стопки. Коли
функція доходить до return, її кадр знімається зі стопки й зникає разом
з усіма іменами всередині, а повернуте значення підставляється в те місце, звідки
викликали.
Найкраще це видно, коли одна функція викликає іншу. Пройди шість кроків: на кроці 2 на стопці буде два кадри одночасно.
Інтерактив 5 · Стек викликів
Ліворуч код, праворуч стопка кадрів. Стопка росте вгору.
підсумок ще не
закінчилась, її кадр лишається на стопці, а зверху лягає кадр
вартість. Коли верхній кадр повертає 57.0, він зникає, і виконання
продовжується з того самого рядка, де перервалось — з присвоєння
базова = ….RecursionError. Побачити межу можна через
sys.getrecursionlimit().07 / СтильРання відмова замість вкладеності
Оскільки return виходить негайно, ним зручно відсікати погані
випадки на початку. Прийом називається рання відмова (early return), і різницю
найпростіше побачити на функції з трьома перевірками.
Природний перший варіант — вкладати if в if: спершу
перевірити кошик, усередині — оплату, усередині — адресу. Виходить драбинка, у якій
корисний рядок опиняється на четвертому рівні відступу, а гілки else
розʼїжджаються так далеко від своїх if, що доводиться водити пальцем.
Другий варіант — перевернути кожну умову й одразу вийти: «немає кошика — повертаємо відмову й на цьому все». Тоді всі перевірки лежать пласко одна під одною, а внизу, без жодного відступу, — випадок, заради якого функція й писалась. Перемикай прапорці: обидві версії дають однакову відповідь у всіх восьми комбінаціях.
Інтерактив 6 · Вкладеність проти ранньої відмови
Підсвічені рядки — ті, що виконались. Постав будь-яку комбінацію прапорців.
08 / ДокументаціяДокстрінг і help()
Ім’я функції каже, що вона робить, але не каже, чого вона чекає на вході й що поверне. Для цього є докстрінг (docstring) — рядок, який стоїть першим у тілі функції:
Це звичайний рядок у потрійних лапках, тому в ньому можна вільно переносити рядки й ставити лапки. Технічно він нічого не робить: Python його обчислює й відкидає — але не зовсім. Він зберігає його в самій функції, і подивитись докстрінг можна прямо з інтерпретатора:
Те саме лежить в атрибуті підсумок.__doc__. Саме звідси беруться
підказки в редакторі коду, коли ти набираєш імʼя функції й натискаєш крапку. І саме
тому help(len) чи help(list.append) працюють: у стандартних
функцій докстрінги теж є.
Що писати всередині: одним реченням — що функція повертає, потім, якщо треба, що означає кожен параметр і які значення вважаються неприпустимими. Не переказуй код («множить ціну на кількість» видно й так) — пиши те, чого з коду не видно: одиниці вимірювання, припущення, поведінку в дивних випадках.
09 / ПриродаФункція — це обʼєкт
Повернімось до теми 03. Там ми домовились: імʼя — це ярлик, а на іншому кінці ярлика живе обʼєкт. Тоді в переліку «усе є обʼєктом» мимохідь згадувалась і функція. Тепер це можна перевірити.
Рядок def підсумок(...) робить рівно те саме, що x = 5:
створює обʼєкт і чіпляє на нього ярлик. Просто обʼєкт цього разу — функція, а ярлик
— її імʼя. Звідси одразу випливає кілька речей, які інакше виглядали б магією:
- Функцію можна присвоїти іншому імені:
порахувати = підсумок. Копії не станеться — буде другий ярлик на той самий обʼєкт, і виклик через будь-який з них дасть однаковий результат. підсумокбез дужок — це сам обʼєкт-функція.підсумок(28.5, 2)з дужками — це виклик, тобто вже результат. Плутанина цих двох записів — окрема класика:print(підсумок)надрукує щось на кшталт<function підсумок at 0x7f2c4a1a7e20>, а не число.- Функцію можна покласти в список чи словник і передати іншій функції як
аргумент. На цьому побудовані
sorted(…, key=…),max(…, key=…)і майже вся робота з даними далі в курсі.
Пройди шість кроків. Схема та сама, що в темі 03, — і це не збіг.
Інтерактив 7 · Імʼя, ярлик і обʼєкт-функція
Ліворуч імена, праворуч обʼєкти в памʼяті. Функція — такий самий обʼєкт, як список.
del вартість зникло
імʼя, а не функція. Обʼєкт живий, бо на нього дивиться другий ярлик, і
порахувати(28.5, 2) працює як працювало. Звертання до
вартість тепер дає NameError.Повернути кілька значень
return віддає рівно одне значення. Але цим значенням може бути
кортеж — і тоді функція фактично повертає
скільки завгодно. Дужки при цьому не потрібні: кома сама створює кортеж.
А на місці виклику спрацьовує розпакування з теми 07:
Технічно повернувся один обʼєкт — кортеж (305.7, 145.0), — а два імені
зʼявились уже на боці виклику. Якщо написати результат = розбір(…) без
розпакування, у результат ляже саме кортеж.
10 / МайстерністьЯк писати добрі функції
Синтаксис ти вже знаєш. Далі — те, що відрізняє робочий код від коду, з яким приємно жити. Ці поради стануть у пригоді не сьогодні, а на третьому місяці, коли власна функція перестане поміщатись на екран.
Одна функція — одна річ
Найкорисніше правило й найважче до виконання. Функція має відповідати на одне питання або виконувати одну дію. Ознака порушення — сполучник «і» в описі того, що вона робить: «рахує суму і друкує чек і зберігає у файл» — це три функції, склеєні в одну.
Чому це не естетика: склеєну функцію неможливо перевикористати частково. Якщо тобі потрібна тільки сума, а функція заразом друкує чек — доведеться або терпіти зайвий друк, або копіювати код. І перевірити її окремо теж не вийде: щоб дізнатись, чи правильна сума, доведеться дивитись на екран замість того, щоб просто порівняти повернуте число з очікуваним.
Чиста функція
Чиста функція (pure function) — це функція, яка задовольняє дві
умови: на однакових аргументах завжди повертає однаковий результат і не змінює нічого
поза собою (не друкує, не пише у файл, не редагує переданий список). Наша
підсумок(ціна, кількість) чиста. Функція, яка друкує чек, — ні.
Практична цінність — у перевірці. Чисту функцію тестують одним рядком: викликаємо з відомими аргументами й порівнюємо з відомою відповіддю.
Такий рядок нічого не друкує, поки все гаразд, і зупиняє програму, щойно результат розійшовся. Це найдешевший спосіб не зламати сьогодні те, що працювало вчора, — і саме його ми використовуватимемо в практиці до кінця курсу. Нечисту функцію так не перевіриш: щоб переконатись, що вона надрукувала правильне, треба перехоплювати вивід.
Це не означає «усі функції мають бути чистими». Друкувати, писати у файл і читати з мережі комусь таки треба. Означає інше: тримай розрахунок і побічні дії в різних функціях. Одна рахує, друга друкує те, що перша порахувала.
return замість прапорця
Типовий пошук у циклі новачок пише через змінну-прапорець: завести
знайдено = False, у циклі поставити True, після циклу
перевірити. Усередині функції це зайве — return посеред циклу і виходить
з циклу, і виходить із функції:
Прапорця немає, break не потрібен, стан ніде не зберігається. Останній
рядок читається як «цикл добіг до кінця й нічого не знайшов». Порівняй із версією
через прапорець — вона на три рядки довша й має додаткове імʼя, яке треба тримати
в голові.
Скільки рядків має бути в функції
Точного числа немає, але є робочий орієнтир: функція має поміщатись на екран без прокручування, тобто десь до 20-30 рядків. Довша — майже завжди ознака, що всередині сховані дві-три окремі дії, і їх варто винести. Так само підозріла велика кількість параметрів: пʼять і більше зазвичай означають, що функція намагається робити забагато.
11 / ПідсумокЩо забрати з теми
Функція — це імʼя, почеплене на шматок коду. Усе інше випливає звідси:
defстворює, дужки викликають. Рядокdefне виконує тіло — він лише робить обʼєкт-функцію й вішає ярлик. Тіло чекає першого виклику.- Параметр — в оголошенні, аргумент — у виклику. Розподіляються за позицією: скільки параметрів, стільки й аргументів, у тому самому порядку.
returnвіддає значення й негайно виходить. Рядки нижче в тілі не виконаються. Тому кількаreturnу різних гілках — нормальна практика, а не хитрість.return— для програми,print— для людини. Функція, яка тільки друкує, повертаєNone, і результат її роботи неможливо використати далі.- Функція без
returnповертаєNone. Це не порожнеча, а конкретний обʼєкт типуNoneType; перевіряють його черезis None. - Кожен виклик — окремий кадр на стеку. Кадр зникає разом
з
return, а значення підставляється в місце виклику. - Функція — звичайний обʼєкт. Її можна присвоїти другому імені, покласти в список, передати в іншу функцію.
- Одна функція — одна річ. Розрахунок окремо, друк окремо. Тоді
перше можна перевірити одним
assert.
Лишилось найцікавіше питання, якого ми тут навмисно не торкались. Що станеться зі
змінною разом, створеною всередині підсумок, після того як
функція завершиться? Чи побачить її решта програми? А чи побачить функція імена, що
живуть зовні? І як передати аргумент за іменем або зробити його необовʼязковим? Це
тема 15 — аргументи й область видимості. Там же зʼясується, чому
змінюваний список як значення за замовчуванням — пастка, яку ти вже бачив в
інтерактиві теми 03.
practice.ipynb ти напишеш
перші власні функції для кошика покупок, побачиш на власному екрані, що
сума_B(…) повертає None, розріжеш довгий скрипт на чотири
функції, додаси докстрінги й прочитаєш їх через help(). Кожен висновок
закріпимо через assert — включно з тим, що функція без return
справді дає None.Далі в темі
Теорію прочитано. Тепер закріпи її на практиці.