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

Функції: імʼя для шматка коду

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

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

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

Наскрізний приклад лишається тим самим кошиком покупок, з яким ми працювали в темі 06 і темі 12:

покупки = ["хліб", "молоко", "яблука", "мед", "сіль"] ціни = {"хліб": 28.5, "молоко": 32.0, "яблука": 19.9, "мед": 145.0, "сіль": 12.0} кількості = [2, 1, 3, 1, 1]

01 / МотиваціяНавіщо взагалі функції

Уяви, що для кожної позиції в чеку треба порахувати вартість зі знижкою 10%. Без функцій це виглядає так: три рядки на позицію, і так стільки разів, скільки позицій.

разом = 28.5 * 2 разом = разом * 0.9 print(round(разом, 2)) разом = 32.0 * 1 разом = разом * 0.9 print(round(разом, 2))

Код працює. Проблема в іншому: формула записана кілька разів. Завтра знижку змінять з 10% на 12% — і треба знайти всі місця, де стоїть 0.9, і виправити кожне. Пропустиш одне — програма й далі рахуватиме, просто інколи неправильно. Це найгірший сорт помилки: без падіння, без повідомлення, з тихо зіпсованими числами.

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

Інтерактив 1 · До і після рефакторингу

Ліворуч — повторений код, праворуч — функція й виклики. Рожевим позначений рядок із формулою знижки.

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

Отже, три причини, і вони різні за вагою:

02 / ВизначенняЯк влаштований def

Функція створюється ключовим словом def (від define — визначити). Ось та сама формула, оформлена як функція:

def підсумок(ціна, кількість): разом = ціна * кількість разом = разом * 0.9 return round(разом, 2)

Розберімо рядок за рядком:

Дужки в цьому рядку порожніми бути можуть: def привітати(): — цілком законна функція без параметрів. А от самі дужки обовʼязкові завжди, навіть порожні.

Визначення не виконує тіло

Тепер найважливіше в цьому розділі, і саме тут новачки спотикаються найчастіше. Коли інтерпретатор доходить до рядка def, він не виконує тіло функції. Він робить дві речі: створює в памʼяті обʼєкт-функцію (усередині якого лежить готовий до виконання код тіла) і чіпляє на цей обʼєкт ярлик із назвою функції. Усе. Тіло не запускається жодного разу.

Тіло виконується тільки тоді, коли функцію викликають — тобто пишуть її імʼя з дужками: підсумок(28.5, 2). Дужки тут і є командою «виконай». Пройди п'ять кроків і подивись, коли саме щось потрапляє у вивід.

Інтерактив 2 · def нічого не друкує

Крок за кроком. Стеж за третім рядком коду — він усередині функції.

виконано рядків0
у виводі0
імʼя «привітати»не існує
Ключовий момент — крок 3. Рядок def уже виконано, імʼя привітати вже існує, а тіло — ні: у виводі досі один рядок. Виконавець перескочив із другого рядка одразу на четвертий. Тіло дочекається свого лише на кроці 5, коли до нього прийдуть через виклик.
Наслідок, який економить нерви. Помилку всередині тіла функції Python не помітить, поки функцію не викличуть. Синтаксичну — помітить одразу, ще при читанні файлу. А от 1 / 0 чи звернення до неіснуючого імені спокійно пролежать у тілі скільки завгодно довго й вистрелять під час першого виклику. Тому «файл запустився без помилок» ще нічого не означає про функції, які в ньому лежать.

03 / ДаніПараметри й аргументи

Два слова, які вживають як синоніми, а насправді вони позначають різні речі — і плутанина тут заважає читати повідомлення про помилки.

Коротко: параметр — це те, що написано в оголошенні; аргумент — те, що прилетіло при виклику. Параметр один на всю функцію, аргументи різні на кожен виклик.

За замовчуванням аргументи розподіляються за позицією: перший аргумент потрапляє в перший параметр, другий — у другий. Тому порядок має значення, і підсумок(2, 28.5) — це не те саме, що підсумок(28.5, 2). Тут, до речі, обидва варіанти дадуть однакове число, бо множення комутативне; у загальному випадку переставлені аргументи дають тихо неправильну відповідь.

Якщо аргументів дати не стільки, скільки параметрів, Python зупинить програму:

>>> підсумок(28.5) TypeError: підсумок() missing 1 required positional argument: 'кількість'

Читається буквально: «бракує одного обовʼязкового позиційного аргументу — кількість». Слово positional у повідомленні — це якраз про розподіл за позицією.

Це ще не вся історія. Аргументи можна передавати за іменем (підсумок(кількість=2, ціна=28.5)), задавати їм значення за замовчуванням, приймати їх довільну кількість. Усе це — тема 15, разом із питанням, що станеться зі змінною, створеною всередині функції. Поки що нам вистачить найпростішого випадку: скільки параметрів оголошено, стільки аргументів і передаємо, у тому самому порядку.

04 / Поверненняreturn: значення й негайний вихід

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

  1. Обчислює вираз праворуч від себе й віддає отримане значення на місце виклику. Рядок ціна_зі_знижкою = підсумок(28.5, 2) перетворюється на ціна_зі_знижкою = 51.3.
  2. Негайно завершує функцію. Не «наприкінці», не «коли дійде до кінця тіла» — просто в цю мить. Усе, що написано в тілі нижче, не виконається ніколи.

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

def знижка(сума): if сума < 200: return 0.0 if сума < 500: return 0.05 return 0.10

Зверни увагу: жодного else тут немає — і він не потрібен. Якщо перший return спрацював, до другої перевірки виконання просто не дійде. Пересунь повзунок і подивись, який саме return спрацьовує й скільки рядків лишається невиконаними.

Інтерактив 3 · return виходить одразу

Змінюй суму. Бірюзові рядки виконано, рожевий — той, що завершив функцію, сірі не виконались.

повернуто
вихід у рядку
рядків після нього не виконано
Порівняй два сірих кольори. Один рядок пропущено, бо умова виявилась хибною — звичайна робота if. Інші пропущено з іншої причини: функція вже закінчилась на return, і виконання до них фізично не дійшло.
Найпоширеніша помилка з кількома return. Якщо жодна гілка не спрацювала, а внизу немає «останнього» return, функція все одно щось поверне — і це буде None. Прибери в прикладі вище рядок return 0.10, і виклик знижка(700) тихо віддасть None замість числа. Помилка вилізе не тут, а рядків через десять, коли хтось спробує це None помножити.

05 / Головна плутанинаreturn проти print

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

def сума_A(ціна, кількість): return ціна * кількість def сума_B(ціна, кількість): print(ціна * кількість)

Різниця в тому, кому призначений результат:

І тепер головне. Функція сума_B не має return — отже, вона повертає None. Рядок y = сума_B(28.5, 2) надрукує 57.0 на екран і покладе в y нічого. Точніше — None. Натискай «наступний крок» і дивись на два стовпчики паралельно.

Інтерактив 4 · Повернути чи надрукувати

Дві майже однакові функції. Однакові дії — різна доля результату.

x
y
type(x)
type(y)
Останній крок і є вирок. x * 2 дає 114.0, бо в x лежить число. y * 2 падає з TypeError, бо в y лежить None — а множити None не можна. Число, яке ти бачив на екрані, програмі так і не дісталось.

None — це не «нічого»

Функція без return повертає None. Це не «порожнеча» і не «відсутність результату» — це цілком конкретний обʼєкт, єдиний свого роду, з власним типом NoneType. У нього просто немає корисного вмісту: він означає «тут значення не передбачене».

>>> результат = сума_B(28.5, 2) 57.0 >>> print(результат) None >>> type(результат) <class 'NoneType'> >>> результат is None True

Зверни увагу на останній рядок: перевіряти на None прийнято через is, а не ==. Причину ми розібрали в темі 03: None існує в програмі в єдиному примірнику, тому питання «це той самий обʼєкт» тут точніше за питання «однакового вмісту».

Правило, яке рятує від половини цієї плутанини: функція рахує й повертає, а друкує той, хто її викликав. Тобто пиши print(підсумок(28.5, 2)), а не ховай print усередину. Тоді ту саму функцію можна використати і щоб показати число людині, і щоб додати його до загальної суми — а функція з print усередині годиться тільки на перше.

06 / МеханікаЩо відбувається під час виклику

Щоб функція могла викликати іншу функцію, а та — третю, і щоб усі вони потім повернулись рівно туди, звідки їх покликали, інтерпретатор веде стек викликів (call stack). Стек — це стопка: класти й брати можна тільки згори.

На кожен виклик Python створює кадр (frame) — коробочку, у якій живуть параметри цієї функції та її локальні імена. Кадр кладеться на верх стопки. Коли функція доходить до return, її кадр знімається зі стопки й зникає разом з усіма іменами всередині, а повернуте значення підставляється в те місце, звідки викликали.

Найкраще це видно, коли одна функція викликає іншу. Пройди шість кроків: на кроці 2 на стопці буде два кадри одночасно.

Інтерактив 5 · Стек викликів

Ліворуч код, праворуч стопка кадрів. Стопка росте вгору.

глибина стеку1
виконується
повернуто
разом
Крок 2 — суть усієї схеми. Функція підсумок ще не закінчилась, її кадр лишається на стопці, а зверху лягає кадр вартість. Коли верхній кадр повертає 57.0, він зникає, і виконання продовжується з того самого рядка, де перервалось — з присвоєння базова = ….
Для тих, хто вже пише код. Ця сама стопка — те, що ти бачиш у traceback. Рядки «File …, line …, in …» перелічені знизу вгору саме в порядку кадрів: найнижчий — де все почалось, найвищий — де впало. І глибина стеку не безмежна: за замовчуванням CPython дозволяє близько 1000 вкладених кадрів, після чого кидає RecursionError. Побачити межу можна через sys.getrecursionlimit().

07 / СтильРання відмова замість вкладеності

Оскільки return виходить негайно, ним зручно відсікати погані випадки на початку. Прийом називається рання відмова (early return), і різницю найпростіше побачити на функції з трьома перевірками.

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

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

Інтерактив 6 · Вкладеність проти ранньої відмови

Підсвічені рядки — ті, що виконались. Постав будь-яку комбінацію прапорців.

вкладеність
рання відмова
результат (обидві версії)
Однакова відповідь, різна форма. Ліворуч 11 рядків і чотири рівні відступу, праворуч 8 рядків і два. Головне не в цифрах: праворуч кожна перевірка стоїть поруч зі своєю відмовою, а щасливий шлях — останнім рядком без відступу. Його видно з першого погляду.

08 / ДокументаціяДокстрінг і help()

Ім’я функції каже, що вона робить, але не каже, чого вона чекає на вході й що поверне. Для цього є докстрінг (docstring) — рядок, який стоїть першим у тілі функції:

def підсумок(ціна, кількість): """Повертає вартість позиції зі знижкою 10%, округлену до копійок. ціна — ціна за одиницю, число кількість — скільки одиниць, ціле число """ разом = ціна * кількість разом = разом * 0.9 return round(разом, 2)

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

>>> help(підсумок) Help on function підсумок in module __main__: підсумок(ціна, кількість) Повертає вартість позиції зі знижкою 10%, округлену до копійок. ...

Те саме лежить в атрибуті підсумок.__doc__. Саме звідси беруться підказки в редакторі коду, коли ти набираєш імʼя функції й натискаєш крапку. І саме тому help(len) чи help(list.append) працюють: у стандартних функцій докстрінги теж є.

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

09 / ПриродаФункція — це обʼєкт

Повернімось до теми 03. Там ми домовились: імʼя — це ярлик, а на іншому кінці ярлика живе обʼєкт. Тоді в переліку «усе є обʼєктом» мимохідь згадувалась і функція. Тепер це можна перевірити.

Рядок def підсумок(...) робить рівно те саме, що x = 5: створює обʼєкт і чіпляє на нього ярлик. Просто обʼєкт цього разу — функція, а ярлик — її імʼя. Звідси одразу випливає кілька речей, які інакше виглядали б магією:

Пройди шість кроків. Схема та сама, що в темі 03, — і це не збіг.

Інтерактив 7 · Імʼя, ярлик і обʼєкт-функція

Ліворуч імена, праворуч обʼєкти в памʼяті. Функція — такий самий обʼєкт, як список.

вартість
порахувати
той самий обʼєкт?
виклик дав
Крок 5 доводить головне: після del вартість зникло імʼя, а не функція. Обʼєкт живий, бо на нього дивиться другий ярлик, і порахувати(28.5, 2) працює як працювало. Звертання до вартість тепер дає NameError.

Повернути кілька значень

return віддає рівно одне значення. Але цим значенням може бути кортеж — і тоді функція фактично повертає скільки завгодно. Дужки при цьому не потрібні: кома сама створює кортеж.

def розбір(ціни, кількості): """Повертає суму чека і найдорожчу позицію.""" сума = 0 найдорожча = 0 for ціна, к in zip(ціни.values(), кількості): сума = сума + ціна * к if ціна * к > найдорожча: найдорожча = ціна * к return сума, найдорожча

А на місці виклику спрацьовує розпакування з теми 07:

>>> усього, максимум = розбір(ціни, кількості) >>> усього 305.7 >>> максимум 145.0

Технічно повернувся один обʼєкт — кортеж (305.7, 145.0), — а два імені зʼявились уже на боці виклику. Якщо написати результат = розбір(…) без розпакування, у результат ляже саме кортеж.

10 / МайстерністьЯк писати добрі функції

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

Одна функція — одна річ

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

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

Чиста функція

Чиста функція (pure function) — це функція, яка задовольняє дві умови: на однакових аргументах завжди повертає однаковий результат і не змінює нічого поза собою (не друкує, не пише у файл, не редагує переданий список). Наша підсумок(ціна, кількість) чиста. Функція, яка друкує чек, — ні.

Практична цінність — у перевірці. Чисту функцію тестують одним рядком: викликаємо з відомими аргументами й порівнюємо з відомою відповіддю.

assert підсумок(28.5, 2) == 51.3 assert підсумок(0, 5) == 0.0

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

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

return замість прапорця

Типовий пошук у циклі новачок пише через змінну-прапорець: завести знайдено = False, у циклі поставити True, після циклу перевірити. Усередині функції це зайве — return посеред циклу і виходить з циклу, і виходить із функції:

def є_дорожче(ціни, межа): """Чи є в списку хоч одна ціна, більша за межу.""" for ціна in ціни: if ціна > межа: return True return False

Прапорця немає, break не потрібен, стан ніде не зберігається. Останній рядок читається як «цикл добіг до кінця й нічого не знайшов». Порівняй із версією через прапорець — вона на три рядки довша й має додаткове імʼя, яке треба тримати в голові.

Скільки рядків має бути в функції

Точного числа немає, але є робочий орієнтир: функція має поміщатись на екран без прокручування, тобто десь до 20-30 рядків. Довша — майже завжди ознака, що всередині сховані дві-три окремі дії, і їх варто винести. Так само підозріла велика кількість параметрів: пʼять і більше зазвичай означають, що функція намагається робити забагато.

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

Функція — це імʼя, почеплене на шматок коду. Усе інше випливає звідси:

Лишилось найцікавіше питання, якого ми тут навмисно не торкались. Що станеться зі змінною разом, створеною всередині підсумок, після того як функція завершиться? Чи побачить її решта програми? А чи побачить функція імена, що живуть зовні? І як передати аргумент за іменем або зробити його необовʼязковим? Це тема 15 — аргументи й область видимості. Там же зʼясується, чому змінюваний список як значення за замовчуванням — пастка, яку ти вже бачив в інтерактиві теми 03.

Далі в практиці. У practice.ipynb ти напишеш перші власні функції для кошика покупок, побачиш на власному екрані, що сума_B(…) повертає None, розріжеш довгий скрипт на чотири функції, додаси докстрінги й прочитаєш їх через help(). Кожен висновок закріпимо через assert — включно з тим, що функція без return справді дає None.

Далі в темі

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