Почему ваш Python-код ведёт себя странно? Ловушка mutable default arguments
Разбираем одну из самых коварных ошибок Python, которая заставляет функции "запоминать" предыдущие вызовы. Узнайте, почему пустой список внезапно становится непустым и как правильно работать с аргументами по умолчанию.
Как выглядит проблема?
Представьте, что вы пишете функцию для добавления задач в список дел:
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 единомышленников в активном телеграм-канале, где всегда помогут разобраться и поддержат на пути в программировании!