Функции
вход, выход и то, что происходит вдобавок
Что это
Чтобы прочитать функцию, не обязательно разбирать её внутренности. Достаточно ответить на три вопроса: что приходит, что уходит и что меняется снаружи.
Третий вопрос — самый важный и самый пропускаемый. Функция может ничего не возвращать и при этом перевернуть полпрограммы.
Как выглядит в коде
# Чистая функция: тот же вход -> тот же выход, ничего больше
def format_price(amount: float, currency: str = "NOK") -> str:
return f"{amount:.2f} {currency}"
# Побочный эффект: меняет то, что ей передали
def add_tax(items: list[dict], rate: float) -> None:
for item in items:
item["price"] = round(item["price"] * (1 + rate), 2)
# Побочный эффект: пишет наружу
def log_order(order_id: int) -> None:
with open("orders.log", "a", encoding="utf-8") as f:
f.write(f"order {order_id}\n")Чистая функция
format_price принимает число и строку, возвращает строку. Вызови её сто раз
с теми же аргументами — сто раз получишь одно и то же, и больше ничего не
произойдёт.
Такие функции проверять легче всего: достаточно сравнить вход и выход.
Функция с побочным эффектом
add_tax возвращает None, но работу делает — меняет словари в переданном
списке. Вот что из этого следует:
cart = [{"name": "кофе", "price": 100.0}]
add_tax(cart, 0.25)
print(cart) # [{'name': 'кофе', 'price': 125.0}]
add_tax(cart, 0.25)
print(cart) # [{'name': 'кофе', 'price': 156.25}]Второй вызов начислил налог на налог. Функция не защищена от повторного вызова, и сама она об этом не скажет — ни по имени, ни по сигнатуре.
Где искать побочные эффекты
Пробегись глазами по телу функции и ищи четыре вещи:
| Признак | Что происходит |
|---|---|
| присваивание в то, что пришло аргументом | меняет данные вызывающего |
open(), запись в файл | меняет диск |
conn.execute(), INSERT, UPDATE | меняет базу |
requests, await session.post() | меняет что-то на другом сервере |
global, присваивание в переменную модуля | меняет состояние программы |
Если ни одного признака нет и есть return — функция чистая, и жизнь
проще.
Аргументы по умолчанию
def format_price(amount: float, currency: str = "NOK") -> str:currency можно не передавать, тогда будет "NOK". Читается легко, но есть
ловушка, которую ИИ воспроизводит регулярно:
def add_item(item, cart=[]): # так нельзя
cart.append(item)
return cartСписок в значении по умолчанию создаётся один раз — и остаётся общим для
всех вызовов. Второй вызов add_item("хлеб") вернёт список из двух элементов.
Правильно — cart=None, а внутри if cart is None: cart = [].
Как сказать ИИ
Для этой функции ответь на три вопроса:
что принимает, что возвращает, что меняет снаружи себя.
По одной строке на вопрос.Что произойдёт, если вызвать эту функцию дважды подряд
с теми же аргументами?Второй вопрос ловит ровно ту категорию ошибок, что в примере с налогом.
На что смотреть
Функция и возвращает, и меняет. Делает две вещи сразу — её трудно переиспользовать и легко вызвать не вовремя. Обычно стоит разделить.
Имя обещает не то. get_user обязана только получать. Если внутри
get_user создаётся запись в базе, имя врёт, и следующий читатель на этом
ошибётся.
Изменяемое значение по умолчанию. =[], ={} в аргументах — почти всегда
ошибка.
Слишком много аргументов. Больше пяти — признак, что функция делает несколько дел. Проверь, не стоит ли её разбить.
Шпаргалка
Три вопроса: что принимает · что возвращает · что меняет снаружи.
Чистая функция: тот же вход — тот же выход, и больше ничего.
-> Noneзначит, что вся работа в побочных эффектах.Побочные эффекты ищи по: записи в аргумент, файлам, базе, сети,
global.=[]и={}в аргументах по умолчанию — ошибка.Спроси: «что будет, если вызвать дважды подряд?»