Логотип коледжу
Оптико-механічний фаховий коледж

Функції: параметри та повернення значень

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

Цілі лекції

Після опрацювання матеріалу ви зможете:

  • розкладати задачу на невеликі функції з одним зрозумілим призначенням;
  • оголошувати й викликати функції через def;
  • розрізняти параметри та аргументи;
  • передавати позиційні, іменовані й стандартні аргументи;
  • повертати одне або кілька значень через return;
  • пояснювати на початковому рівні, як Python передає посилання на об'єкти;
  • розрізняти локальні й глобальні імена та читати схему LEGB;
  • впізнавати чисті функції та доречно використовувати короткі lambda-вирази.

Передумови та межі

Потрібно знати змінні, числові типи, арифметичні вирази, if і цикли. Основна мова прикладів - Python; Scala й Go показують той самий задум з іншим синтаксисом.

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

1. Навіщо виділяти функції

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

  1. відкалібрувати вимірювання;
  2. перевірити межі;
  3. подати результат користувачеві.

Декомпозиція - поділ великої задачі на менші частини. Хороша функція зазвичай виконує одну змістовну дію, має промовисту назву та чітко визначені вхід і вихід.

Порівняйте назви f(x) і calibrate_voltage(raw_voltage). Обидві допустимі синтаксично, але друга пояснює задум без читання тіла. Назви функцій у Python зазвичай записують малими літерами зі знаком підкреслення: calibrate_voltage, is_in_range, format_result.

Функції дають кілька переваг:

  • повторне використання - алгоритм записано один раз;
  • читабельність - назва пояснює роль фрагмента;
  • локальне виправлення - формулу змінюють в одному місці;
  • перевірка частинами - невелику функцію легше протестувати;
  • комбінування - результат однієї функції стає аргументом іншої.

Передавання аргументів у функцію та повернення результату

2. Оголошення def і виклик

У Python оголошення починається ключовим словом def. Після назви в дужках записують параметри, після двокрапки - тіло з відступом. Код усередині функції виконується не під час оголошення, а під час виклику.

def calibrate_voltage(raw_voltage, gain, offset):
    calibrated = raw_voltage * gain + offset
    return calibrated


voltage = calibrate_voltage(2.4, 1.05, -0.02)
print(voltage)

Очікуване значення близьке до 2.5. Через подання дробових чисел у пам'яті останні цифри виводу можуть відрізнятися.

У Scala 3 після двокрапок зазначено типи параметрів і результату; останній вираз calibrated стає результатом функції. У Go оголошення починається func, тип результату вказано перед тілом, а return записано явно. У Python типи в цьому прикладі не оголошено, але функція все одно очікує значення, для яких визначені множення й додавання.

Оголошення не є викликом

Після виконання лише def calibrate_voltage(...): ... Python створює функцію, але не обчислює напругу. Дужки з конкретними даними виконують виклик: calibrate_voltage(2.4, 1.05, -0.02).

Функцію треба визначити до моменту, коли виконання програми дійде до її виклику. Тому оголошення зазвичай розміщують вище за основну послідовність команд.

3. Параметри та аргументи

Ці терміни описують різні ролі:

  • параметр - ім'я в оголошенні функції;
  • аргумент - конкретне значення або вираз у виклику.

У def calibrate(raw_value, gain): імена raw_value і gain є параметрами. У calibrate(510, 0.02) числа 510 і 0.02 є аргументами. Під час виклику параметри отримують посилання на передані об'єкти й діють як локальні імена функції.

Позиційні аргументи

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

def optical_power(voltage, sensitivity):
    return voltage / sensitivity


power = optical_power(2.5, 0.5)
print(power)

Результат: 5.0 у Python і Scala, 5 у типовому виводі Go. Якщо поміняти аргументи місцями, формула дасть інший результат. Мова не знає фізичного змісту чисел і не виправить логічну помилку автоматично.

Іменовані аргументи

У Python можна явно вказати ім'я параметра. Це особливо корисно, коли кілька аргументів мають однаковий тип або коли потрібно пропустити частину стандартних значень.

def calibrate(raw_value, gain=1.0, offset=0.0):
    return raw_value * gain + offset


result = calibrate(raw_value=510, offset=-0.2, gain=0.02)
print(result)

Python і Scala підтримують іменовані аргументи, тому в їхніх викликах порядок gain та offset не впливає на зіставлення. Go не має іменованих аргументів: значення передаються за позицією.

У Python позиційні аргументи мають стояти перед іменованими. Виклик calibrate(raw_value=510, 0.02) є синтаксично неправильним. Не можна також передати той самий параметр двічі: calibrate(510, raw_value=520).

Стандартні значення параметрів

У gain=1.0 та offset=0.0 праві частини є стандартними значеннями. Якщо відповідний аргумент не передано, Python використовує їх:

  • calibrate(500) дає 500.0;
  • calibrate(500, gain=0.02) дає 10.0;
  • calibrate(500, offset=-2.0) дає 498.0.

Параметри без стандартного значення записують перед параметрами зі стандартним значенням. Scala також підтримує стандартні аргументи. У Go їх немає; за потреби створюють окрему функцію-обгортку або передають усі значення явно.

Пастка змінного стандартного аргументу

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

Небезпечно: def record(value, history=[]): ...
Безпечно:   def record(value, history=None): ...

У безпечному варіанті всередині функції створюють новий список, коли history is None. Детальніше цю помилку розберемо в сценарії виконання, а правило для початку просте: змінний об'єкт не робіть стандартним значенням параметра.

4. return: результат замість показу на екрані

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

def is_wavelength_visible(wavelength_nm):
    return 380 <= wavelength_nm <= 780


visible = is_wavelength_visible(532)
print(visible)

Результат логічний: True у Python, true у Scala й Go.

print(value) показує значення людині в консолі, але не передає його програмі як результат функції. Якщо у функції немає виконаного return, Python повертає спеціальне значення None.

Корисний поділ відповідальності:

  • функція обчислення повертає значення;
  • код взаємодії з користувачем друкує повернений результат.

Таку функцію можна використати і в консолі, і в тесті, і в майбутньому графічному інтерфейсі.

Дострокове повернення

return може завершити функцію раніше. Це зручно для перевірки неприйнятних даних:

def classify_signal(intensity):
    if intensity < 0:
        return "помилкове значення"
    if intensity > 100:
        return "насичення"
    return "нормальний сигнал"


print(classify_signal(108))

Очікуваний текст: насичення. У Scala приклад повертає результат умовного виразу без явного return; Python і Go тут використовують return.

5. Кілька значень із функції

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

def calibration_limits(center, tolerance):
    return center - tolerance, center + tolerance


lower, upper = calibration_limits(532, 2)
print(lower, upper)

Очікувані межі: 530 і 534 (формат дробової частини залежить від мови). У Python і Scala функція повертає один складений об'єкт-кортеж. Go має окрему мовну можливість кількох повернених значень; це не кортеж.

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

6. Як Python передає параметри

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

Наслідок залежить від дії всередині функції:

  • переприсвоєння параметра іншому об'єкту не переприсвоює ім'я виклику;
  • зміна переданого змінного об'єкта видима після виклику.
def add_reference_reading(values):
    values.append(532)


readings = [405, 450]
add_reference_reading(readings)
print(readings)

Python надрукує [405, 450, 532]: обидва імені посилалися на той самий список, а append змінив цей список. Scala-приклад навмисно використовує змінний ArrayBuffer, тому зміна також видима. У Go зріз передається за значенням, але містить опис спільного базового масиву; зміна наявного елемента видима виклику. Додавання до зрізу в Go має додаткові правила місткості, тому приклад змінює елемент, а не додає його.

Для чисел, рядків і кортежів ситуація простіша: ці об'єкти незмінні. Операція на кшталт value = value + 1 прив'язує локальне ім'я до нового числа й не змінює зовнішнє ім'я.

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

7. Області видимості та LEGB

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

Python шукає ім'я за правилом LEGB:

ЛітераОбластьДе шукати
LLocalпоточна функція
EEnclosingзовнішня функція для вкладеної функції
GGlobalрівень поточного файла
BBuilt-inвбудовані імена Python, наприклад print, len

Пошук зупиняється на першому знайденому імені. Якщо локальна змінна має ім'я len, вона затінює вбудовану функцію len у цій області. Саме тому не варто називати змінні list, sum, min, max або print.

reference_wavelength = 532


def deviation(measured):
    local_difference = measured - reference_wavelength
    return local_difference


print(deviation(535))

Результат: 3. Ім'я local_difference доступне лише всередині Python-функції. Глобальна константа-подібна величина доступна для читання, але прихована залежність ускладнює перевірку функції. Краще передавати значення явно:

deviation(measured, reference_wavelength)

Ключове слово global дозволяє функції переприв'язати глобальне ім'я, а nonlocal - ім'я із зовнішньої функції. Вони існують, але на початку навчання краще не використовувати їх для звичайних обчислень: явні параметри й return роблять рух даних зрозумілішим. Детальні вкладені функції не є метою цієї лекції.

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

8. Інтуїція чистої функції

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

Функція calibrate(raw_value, gain, offset) є близькою до чистої: результат визначається аргументами. Функція, яка читає глобальний коефіцієнт, змінює список і друкує повідомлення, має кілька прихованих ефектів.

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

9. Короткі функції lambda

У Python lambda створює невелику анонімну функцію з одного виразу. Після двокрапки не можна записати кілька команд або звичайний блок if.

normalize = lambda value, maximum: value / maximum

print(normalize(75, 100))

Результат: 0.75. У Scala аналогічний запис називають функціональним літералом, у Go - анонімною функцією.

Якщо функція має важливу назву, кілька кроків, перевірки або потребує пояснення, звичайний def читається краще. lambda доречна як коротка функція, яку одразу передають іншій операції. Не скорочуйте код лише заради меншої кількості рядків.

10. Як проєктувати функцію

Перед написанням коду дайте відповіді на п'ять питань:

  1. Яку одну дію виконує функція?
  2. Які дані вона отримує як параметри?
  3. Що саме повертає?
  4. Чи змінює вона передані об'єкти або зовнішній стан?
  5. Які граничні значення потрібно перевірити?

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

Підсумок

  • Функція надає ім'я завершеній дії та допомагає декомпозувати програму.
  • Python оголошує функцію через def, а виконує її лише під час виклику.
  • Параметри записані в оголошенні, аргументи передаються у виклику.
  • Позиційні аргументи зіставляються за порядком; іменовані - за назвою; стандартні значення дозволяють пропустити частину аргументів.
  • Змінний об'єкт не слід використовувати як стандартний аргумент, якщо функція його змінює.
  • return передає результат програмі; print лише показує значення. Без виконаного return Python повертає None.
  • Кілька значень return у Python утворюють кортеж, який можна розпакувати.
  • Параметр Python є локальним ім'ям для переданого об'єкта: зміна спільного змінного об'єкта може бути видима після виклику.
  • LEGB задає порядок пошуку імен: Local, Enclosing, Global, Built-in.
  • Чисті функції залежать від явних аргументів і не мають прихованих побічних ефектів.
  • lambda підходить для короткого виразу; складну або важливу дію краще оголосити через def.
  • Рекурсія та модулі залишаються для наступної лекції.

Завдання

1. Яке ключове слово починає оголошення звичайної функції в Python?

2. Як називаються імена `raw_value` і `gain` у заголовку `def calibrate(raw_value, gain):`? Запишіть одне слово у множині.

3. Який виклик правильно передає іменовані аргументи функції `calibrate(raw_value, gain=1.0, offset=0.0)`?

4. Який числовий результат поверне `calibrate(500, gain=0.02)`, якщо `def calibrate(raw_value, gain=1.0, offset=0.0): return raw_value * gain + offset`? Запишіть відповідь з однією цифрою після крапки.

5. Що фактично повертає Python-функція після `return minimum, maximum`?

6. Що станеться зі списком `readings` після виклику функції, яка виконує `values.append(532)`, якщо `readings` передано як аргумент `values`?

7. Яке слово в абревіатурі LEGB відповідає області імен усередині поточної функції? Запишіть англійське слово.