Программирование с ИИ
ENRU
2.5

Функции

вход, выход и то, что происходит вдобавок

Что это

Чтобы прочитать функцию, не обязательно разбирать её внутренности. Достаточно ответить на три вопроса: что приходит, что уходит и что меняется снаружи.

Третий вопрос — самый важный и самый пропускаемый. Функция может ничего не возвращать и при этом перевернуть полпрограммы.

Как выглядит в коде

Схема из книги — будет перерисована для сайта
Функцию читают по трём направлениям. Побочные эффекты — то, что происходит вдобавок, и то, что обычно забывают проверить.
# Чистая функция: тот же вход -> тот же выход, ничего больше
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.

  • =[] и ={} в аргументах по умолчанию — ошибка.

  • Спроси: «что будет, если вызвать дважды подряд?»