Functions
Functions - Функції⚑
Що таке функція у Python⚑
Summary
Функція - це блок коду, який виконується лише тоді, коли його викликають. Функції використовуються для організації коду та повторного використовуваним. Функцію оголошують за допомогою ключового слова
def, за яким слідує ім'я функції та параметри у круглих дужках.
Тіло функції - це блок коду, який виконується при виклику функції. Він може містити різні операції та вказівки. Функція може повертати значення за допомогою ключового слова return. Це значення може бути використане у коді, який викликав функцію.
Для виклику функції використовується її ім'я та передаються аргументи у круглих дужках. Параметри - це значення, які функція отримує при виклику. Аргументи - це конкретні значення, передані функції при її виклику.
Параметри передаються
- по значенню - immutable створюються копії
- по посиланню - по reference - mutable - створюється посилання
В Python функція - це об'єкт. Все що є за межами функцій і класів - глобальні змінні.
Що таке args, kwargs і в яких випадках вони потрібні⚑
Вирази *args і **kwargs оголошуються в сигнатурі функції. Вони означають, що всередині функції будуть доступні змінні з іменами args і kwargs (без зірочок). Можна використовувати інші імена, але це вважається поганим стилем.
args - це кортеж, який накопичує позиційні аргументи. kwargs - словник іменованих аргументів, де ключ - це ім'я параметра, а значення - значення параметра.
Якщо в функцію не передано жодних параметрів, змінні будуть відповідно пустим кортежем і пустим словником, а не None.
Чому використання змінних об'єктів як параметрів за замовчуванням погана практика⚑
Функція створюється один раз під час завантаження модуля. Іменовані параметри і їх значення за замовчуванням також створюються один раз і зберігаються в одному з полів об'єкта-функції (__defaults__). Це стосується списків, множин і словників.
У даному прикладі bar має значення порожнього списку. Список - це змінний об'єкт, тому значення bar може змінюватися від виклику до виклику.
def foo(bar=[]):
bar.append(1)
return bar
foo()
>>> [1]
foo()
[1, 1]
foo()
>>> [1, 1, 1]
foo.__defaults__
>>> ([1, 1, 1],)
Хорошим тоном вважається вказувати для параметра пусте незмінне значення, наприклад 0, None, '', False. В тілі функції перевіряти на False/None і створювати нову колекцію:
def foo(bar=None):
if bar is None:
bar = []
bar.append(1)
return bar
foo()
>>> [1]
foo()
>>> [1]
foo()
>>> [1]
foo.__defaults__
>>> (None,)
Чи можна передавати функцію як аргумент іншій функції⚑
Так, Функції Python є об'єктами першого класу. Їх можна присвоювати змінним, зберігати в структурах даних, передавати як аргументи іншим функціям, і повертати як значення з інших функцій.
Як повернути кілька значень з функції⚑
Summary
Технічно функція завжди повертає один об'єкт. Кілька значень "повертаються" як кортеж:
return a, b, c- синтаксичний цукор дляreturn (a, b, c). На місці виклику використовується tuple-unpacking:x, y, z = func(). Для двох-трьох значень - звичайний кортеж; для більших або іменованих наборів -NamedTupleабоdataclass.
Кортеж + unpacking
def min_max(items):
return min(items), max(items) # Implicit tuple
lo, hi = min_max([3, 1, 4, 1, 5, 9, 2, 6])
# lo = 1, hi = 9
Дужки навколо return min(items), max(items) опціональні: компілятор бачить кому і збирає кортеж автоматично. Це той самий механізм, що й у a, b = 1, 2.
Ігнорування непотрібних значень
Якщо частина повертається, але не потрібна, прийнятна конвенція - _:
_, hi = min_max([3, 1, 4]) # Only need the max
*_, last = (1, 2, 3, 4) # Star-unpacking for "everything but last"
Іменовані результати: NamedTuple або dataclass
Як тільки повертається 3+ значень, читабельність на місці виклику падає - що означає result[2]? Тоді кортеж замінюють на іменовану структуру:
from typing import NamedTuple
class Statistics(NamedTuple):
mean: float
median: float
stdev: float
def compute_stats(items: list[float]) -> Statistics:
...
return Statistics(mean=m, median=md, stdev=sd)
stats = compute_stats(data)
print(stats.mean, stats.stdev) # Self-documenting access
mean, _, _ = stats # Still unpacks like a tuple
NamedTuple - незмінний, легкий, indexable і unpackable одночасно. @dataclass (див. розділ у class_and_object.md) - коли потрібна мутабельність або кастомні методи.
Словник для динамічного набору
Якщо набір полів варіюється або частина може бути відсутньою:
Для public-API краще TypedDict для типобезпеки на статичному аналізі.
Чого не робити
return a; return b- другийreturnніколи не виконається. Якщо потрібно повертати один із двох - умовний оператор:return a if cond else b.- Глобальні змінні як "додаткові повернення" - функція стає прихованим side-effect-кодом, тестувати і композувати її складно.
- Кортежі з 5+ елементів без іменування - заміна на
NamedTuple/dataclassмайже завжди виграш у читабельності і гнучкості (новий field не ламає позиційні розпаковки на сайтах виклику).
Чи можна оголошувати функцію всередині іншої функції. Де вона буде видима⚑
Так, можна. Така функція буде видимою лише всередині першої функції.
Що таке лямбди. Які їх особливості⚑
Лямбда (lambda) - це анонімна функція, яка не резервує ім'я в просторі імен.
Лямбда може приймати необмежену кількість аргументів та мати будь-яку кількість параметрів. Однак лямбда-функція може мати лише один вираз або інструкцію, і вона завжди повертає результат цього виразу.
Лямбди використовується в ситуаціях, коли потрібна анонімна функція на короткий період часу. Наприклад передати в map, reduce, filter, або для сортування.
У Python лямбди можуть складатися лише з одного виразу. Використовуючи синтаксис дужок, можна оформити тіло лямбди у декілька рядків.
Не можна використовувати крапку з комою для розділення операторів.
a = lambda x, y: x + y
print(a(5, 6)) # 11
students = [{"name": "Alice", "age": 25}, {"name": "Bob", "age": 20}]
sorted(students, key=lambda x: x["age"]) # [{"name": "Bob", "age": 20}, {"name": "Alice", "age": 25}]
Наступні вирази при завантаженні модуля викличуть SyntaxError. Тіло лямбди може містити лише вирази. pass і raise є операторами.
Як передаються значення аргументів у функцію або метод (by value or reference)?⚑
Summary - Незмінні (імутабельні) об'єкти передаються "по значенню" (цілі числа, рядки). - Змінювані об'єкти такі, як списки, словники, передаються у вигляді посилань на об'єкти.
У мовах програмування, таких як C++, існують змінні, які зберігаються у стеку та динамічній пам'яті. При виклику функції ми поміщаємо всі аргументи у стек, після чого передаємо керування функції. Функція знає розміри та зміщення змінних у стеку та може вірно їх інтерпретувати.
При цьому у нас є два варіанти: скопіювати пам'ять змінної на стек або помістити посилання на об'єкт у динамічній пам'яті (або на більш високому рівні стеку). Очевидно, що при зміні значень на стеці функції, значення в динамічній пам'яті не зміняться, а при зміні області пам'яті за посиланням, ми змінюємо спільну пам'ять, відповідно всі посилання на цю ж область пам'яті "побачать" нове значення.
У Python відмовилися від подібного механізму, заміною є механізм присвоєння (assignment) імені змінної до об'єкту, наприклад, при створенні змінної: var = "john" інтерпретатор створює об'єкт "john" та "ім'я" var, а потім зв'язує об'єкт з даним іменем. При виклику функції, нових об'єктів не створюється, замість цього в її області видимості створюється ім'я, яке зв'язується з існуючим об'єктом.
Проте в Python є змінні і незмінні типи даних. До других, наприклад, належать числа: при арифметичних операціях існуючі об'єкти не змінюються, а створюється новий об'єкт, з яким потім зв'язується існуюче ім'я. Якщо після цього зі старим об'єктом не зв'язано жодного імені, воно буде видалено за допомогою механізму підрахунку посилань. Якщо ж ім'я зв'язано зі змінною змінного типу, то при операціях з нею змінюється пам'ять об'єкта, відповідно всі імена, зв'язані з даною областю пам'яті, "побачать" зміни.
Links
- How do I write a function with output parameters (call by reference)?
- Интересности и полезности python. Часть 3
Що таке замикання (Closure)⚑
Summary
Замикання (closure) – це вкладена функція, яка дозволяє отримати доступ до змінних зовнішньої функції навіть після завершення виконання зовнішньої функції.
Замикання — це конструкція, яка дозволяє функції "запам'ятовувати" значення змінних з її зовнішнього оточення, навіть після того, як ці змінні вийшли з області видимості. Це досягається завдяки вкладеним функціям, які можуть мати доступ до змінних з оточуючої функції.
Синтаксично це виглядає як функція, що повністю знаходиться у тілі іншої функції. При цьому вкладена внутрішня функція містить посилання на локальні змінні зовнішньої функції. Коли зовнішня функція завершує виконання, контекст її виконання не зникає повністю. Замість цього, він зберігається у вигляді замикання.
def counter():
count = 0
def increment():
nonlocal count
count += 1
print(f"Current count: {count}")
return increment
counter1 = counter()
counter1() # "Current count: 1"
counter1() # "Current count: 2"
print(counter1.__closure__) # return tuple with cell objects
Під капотом використовується атрибут функції, який називається __closure__, який повертає кортеж об'єктів cell, що зберігають значення з області видимості функції, в якій була створена вкладена функція. Кожен елемент кортежу являє собою значення однієї із змінних, що використовуються в замиканні.
Сценарії застосування
- Реалізація декораторів. Декоратор - це майже завжди замикання: внутрішня функція
wrapperпосилається на функцію-аргументfuncзовнішньої функції-декоратора. - Фабрики функцій. Створення функцій із зафіксованими параметрами:
def power_of(exponent):
def raise_to(base):
return base ** exponent
return raise_to
square = power_of(2)
cube = power_of(3)
square(5) # 25
cube(5) # 125
- Інкапсуляція стану без створення класу - "приватні" змінні живуть у замиканні, доступні лише через інтерфейс внутрішньої функції (приклад вище з
counter). - Функціональне програмування - передача поведінки з зафіксованим контекстом замість об'єктів зі станом.
Пастка з присвоюванням
Замикання захоплює змінну зовнішнього scope, лише якщо вона не присвоюється у внутрішній функції. Присвоювання робить ім'я локальним і ламає захоплення:
def make_counter():
count = 0
def inc():
count += 1 # SyntaxError-ish at runtime: UnboundLocalError
return count
return inc
Тут Python бачить count = ... всередині inc і вважає count локальною змінною з самого початку функції; count += 1 намагається зчитати неіснуючу локальну. Щоб змінювати захоплену змінну, потрібен nonlocal:
Для зміни глобальної змінної - аналогічно, але global замість nonlocal. Якщо замикання лише читає змінну (не присвоює), ніяких ключових слів не потрібно.
Чиста функція (pure function)⚑
Summary
Чиста функція - функція, яка задовольняє дві умови: (1) детермінованість - однакові вхідні дані завжди дають однаковий результат; (2) відсутність побічних ефектів - не змінює стан поза собою (глобальні змінні, mutable-аргументи, файли, БД, мережа, time, random). Виклик можна замінити на результат без зміни поведінки програми (referential transparency).
Приклади
Чиста:
Не чисті - кожна порушує одну з умов:
counter = 0
def bump():
global counter
counter += 1 # Side effect: mutates global
return counter
def now():
return datetime.now() # Non-deterministic: depends on system time
def append_log(item, log=[]):
log.append(item) # Mutates argument (and shared default)
return log
def fetch_user(uid):
return db.query(uid) # I/O - non-deterministic, side-effect of DB read
Чому це важливо
- Тестування: пряма перевірка
assert add(2, 3) == 5без mock, fixture, cleanup. Не потрібно ізолювати глобальний стан між тестами. - Мемоізація:
functools.cacheкоректний тільки для чистих функцій. Для не чистих кеш стає bug-ом -now()повертатиме перший виклик завжди. - Паралелізм: чисті функції тривіально розпаралелюються - ніяких race conditions, тому що нема спільного стану.
- Композиція: чисті функції безпечно комбінувати через
f(g(x))- результат не залежить від порядку допоміжних операцій. - Reasoning: код стає локальним; можна зрозуміти функцію за її сигнатурою без читання того, що навколо.
Прагматичний компроміс
100% чиста програма не може робити нічого корисного - їй ніколи не побачити введення користувача чи записати в БД. На практиці:
- ядро бізнес-логіки тримається чистим - функції обчислюють, не торкаються I/O;
- I/O-операції (БД, мережа, файли, час) виштовхуються на межі програми - composition root, controller, adapter;
- mutability дозволена локально в межах функції (накопичувач у циклі), але не виходить за її межі.
Цей патерн відомий як functional core, imperative shell (Gary Bernhardt, "Boundaries", 2012).
Зв'язок з ФП і референційною прозорістю
В чисто-функціональних мовах (Haskell, Elm) чистота - властивість типу: побічні ефекти явно позначені монадами (IO). У Python чистота - конвенція, дотримання якої залежить від дисципліни команди. Детальніше про ФП-підхід - у functional_programming.md.
Links
- Gary Bernhardt: Boundaries (PyCon 2013) - functional core, imperative shell
- Wikipedia: Pure function