No history yet

Ловушки аргументов функций

Скрытая ловушка в аргументах функций

При создании функций в Python мы часто задаем значения по умолчанию для аргументов. Для простых типов, таких как числа или строки, это работает предсказуемо. Но когда в дело вступают изменяемые типы данных, например, списки или словари, возникает неочевидная проблема.

Проблема кроется в том, когда Python создает этот объект по умолчанию. Он создается не каждый раз при вызове функции, а только один раз — в момент определения самой функции. Это означает, что все вызовы функции, которые не передают свой собственный список, будут использовать один и тот же, общий экземпляр списка.

Объект по умолчанию создается один раз и живет вместе с функцией.

Представьте, что вы создали API-эндпоинт в для добавления товаров в корзину пользователя. Код может выглядеть так:

from fastapi import FastAPI

app = FastAPI()

# Неправильный подход!
def add_to_cart(item: str, cart=[]):
    cart.append(item)
    return {"cart": cart}

@app.get("/add-item/{item}")
def handle_add_item(item: str):
    return add_to_cart(item)

Первый пользователь добавляет в корзину «яблоко». Сервер отвечает: {"cart": ["яблоко"]}. Все отлично.

Затем второй пользователь, в совершенно другом сеансе, добавляет «банан». И тут происходит нечто странное. Сервер отвечает ему: {"cart": ["яблоко", "банан"]}. В его корзине оказалось яблоко первого пользователя! Это происходит потому, что оба вызова использовали один и тот же объект списка cart, который был создан при запуске сервера.

Правильный подход: None по умолчанию

Чтобы избежать этой проблемы, никогда не используйте изменяемые объекты в качестве значений по умолчанию. Вместо этого используйте None и создавайте новый объект уже внутри функции. Этот паттерн гарантирует, что каждый вызов функции будет работать с собственным, изолированным экземпляром данных.

Давайте исправим наш пример. Мы также добавим подсказку типа Optional из модуля typing, чтобы явно указать, что cart может быть либо списком, либо None.

from typing import List, Optional
from fastapi import FastAPI

app = FastAPI()

# Правильный подход!
def add_to_cart(item: str, cart: Optional[List[str]] = None):
    # Если корзина не передана, создаем новую
    if cart is None:
        cart = []
    cart.append(item)
    return {"cart": cart}

@app.get("/add-item/{item}")
def handle_add_item(item: str):
    return add_to_cart(item)

Теперь все работает как надо. Когда первый пользователь добавляет «яблоко», создается новый список ['яблоко']. Когда приходит второй пользователь, для него создается свой, совершенно новый и пустой список, в который добавляется «банан». Данные пользователей больше не смешиваются.

Жизненный цикл объектов в FastAPI

Эта проблема особенно важна в контексте веб-серверов, таких как FastAPI. Ваш сервер — это долгоживущий процесс. Объекты, созданные при его запуске (как наш список по умолчанию), продолжают существовать и обслуживать тысячи запросов. Это не похоже на запуск простого скрипта, который завершается через секунду.

Система зависимостей FastAPI также подчиняется этому правилу. Если вы используете в функции-зависимости, этот объект будет общим для всех запросов, которые используют эту зависимость. Это может привести к трудноуловимым ошибкам, когда состояние одного пользователя «протекает» к другому.

Всегда используйте None в качестве заполнителя для опциональных аргументов, которые должны быть списками или словарями.

Закрепим материал небольшим тестом.

Quiz Questions 1/4

Когда в Python создается изменяемый объект, используемый в качестве значения по умолчанию для аргумента функции?

Quiz Questions 2/4

Рассмотрите следующий код на FastAPI:

from fastapi import FastAPI

app = FastAPI()

def process_items(items: list = []):
    items.append("новое значение")
    return {"items": items}

@app.get("/")
def read_root(processed = Depends(process_items)):
    return processed

Что произойдет, если два разных пользователя последовательно сделают запрос к эндпоинту /?