Почему ваш Python-код ведёт себя странно? Ловушка mutable default arguments

Разбираем одну из самых коварных ошибок Python, которая заставляет функции "запоминать" предыдущие вызовы. Узнайте, почему пустой список внезапно становится непустым и как правильно работать с аргументами по умолчанию.

РазработкаPython

6 мин

Как выглядит проблема?

Представьте, что вы пишете функцию для добавления задач в список дел:

def add_task(task, todo_list=[]):
    todo_list.append(task)
    return todo_list

# Добавляем первую задачуprint(add_task("Купить молоко"))  # ['Купить молоко']

# Добавляем вторую задачуprint(add_task("Погулять с собакой"))  # Ожидаем: ['Погулять с собакой']
                                        # Получаем: ['Купить молоко', 'Погулять с собакой']

⚠️ Что произошло? Мы ожидали получить новый список с одной задачей, но вторая задача добавилась к первой! Причём мы нигде не сохраняли предыдущий список. Магия? Нет, это mutable default arguments.

Почему так происходит?

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

Давайте посмотрим, что на самом деле происходит:

def add_task(task, todo_list=[]):
    todo_list.append(task)
    print(f"ID списка: {id(todo_list)}")
    return todo_list

add_task("Первая задача")   # ID списка: 140234567891234add_task("Вторая задача")    # ID списка: 140234567891234 (тот же самый!)add_task("Третья задача")    # ID списка: 140234567891234 (и снова он!)

💡 Ключевой момент: Каждый раз используется один и тот же объект списка. Все вызовы функции работают с одним и тем же списком в памяти, поэтому все изменения накапливаются.

Какие типы данных затронуты?

Эта проблема касается всех изменяемых (mutable) типов данных в Python. Сюда относятся списки, словари, множества и пользовательские объекты. А вот числа, строки и кортежи (immutable типы) работают нормально, потому что их нельзя изменить, можно только создать новые.

# Проблемный код со словарёмdef add_user(name, users_dict={}):
    users_dict[name] = True
    return users_dict

print(add_user("Алексей"))  # {'Алексей': True}print(add_user("Мария"))    # {'Алексей': True, 'Мария': True} - оба здесь!

# Безопасный код со строкой (immutable)def greet(name, greeting="Привет"):
    greeting = greeting + ", " + name
    return greeting

print(greet("Иван"))   # Привет, Иванprint(greet("Ольга"))  # Привет, Ольга - всё работает как ожидается

Правильное решение.

Классическое решение этой проблемы - использовать None как значение по умолчанию, а потом создавать новый объект внутри функции:

def add_task(task, todo_list=None):
    if todo_list is None:
        todo_list = []
    todo_list.append(task)
    return todo_list

# Теперь всё работает правильноprint(add_task("Купить молоко"))        # ['Купить молоко']print(add_task("Погулять с собакой"))   # ['Погулять с собакой']

✅ Правило: Каждый раз, когда мы не передаём список, создаётся новый пустой список. Проблема решена!

То же самое работает и для словарей:

def create_profile(name, data=None):
    if data is None:
        data = {}
    data['name'] = name
    data['created_at'] = 'сегодня'
    return data

profile1 = create_profile("Алексей")
profile2 = create_profile("Мария")

print(profile1)  # {'name': 'Алексей', 'created_at': 'сегодня'}print(profile2)  # {'name': 'Мария', 'created_at': 'сегодня'}

Когда это может быть полезно?

Интересно, что иногда это поведение можно использовать специально, например, для кеширования результатов:

def get_config(config_cache={}):
    if 'data' not in config_cache:
        print("Загружаем конфигурацию...")
        config_cache['data'] = "важные настройки"
    return config_cache['data']

print(get_config())  # Загружаем конфигурацию... важные настройкиprint(get_config())  # важные настройки (уже не загружаем)

💡 Примечание: Но в реальном коде для кеширования лучше использовать специальные инструменты вроде functools.lru_cache, чтобы код был понятнее.

Как избежать этой ошибки?

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

Эта ошибка настолько частая, что многие линтеры (инструменты для проверки кода) специально предупреждают о ней. Если вы используете PyCharm, VSCode с pylint или flake8, вы увидите предупреждение при попытке написать такой код.

Изучайте Python правильно с первого раза!

Эту и множество других важных тем можно изучить подробно в Кодике - разобрать все нюансы с понятными объяснениями и закрепить на практике с интересными заданиями. Каждая тема сопровождается примерами из реальной разработки, а практические задания помогают сразу применить полученные знания.

А если в процессе обучения возникнут вопросы или захочется обсудить сложные моменты, у нас уже больше 2000 единомышленников в активном телеграм-канале, где всегда помогут разобраться и поддержат на пути в программировании!

Комментарии