Проблема изменяемого аргумента по умолчанию
Условие
Что выведет следующий код и почему?
def f(value, items=[]):
items.append(value)
return items
print(f(1))
print(f(2))
print(f(3))
Задача
- Объясните поведение
- Исправьте функцию так, чтобы она работала корректно
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Проблема изменяемого аргумента по умолчанию
Это одна из самых коварных ошибок в Python, которую допускают даже опытные разработчики. Давайте разберёмся в механизме проблемы и способах её решения.
1. Поведение и вывод
def f(value, items=[]):
items.append(value)
return items
print(f(1)) # [1]
print(f(2)) # [1, 2]
print(f(3)) # [1, 2, 3]
Неожиданный вывод! Третий вызов выводит всю историю добавленных значений, а не только [3].
2. Почему это происходит?
В Python списки создаются один раз при определении функции, а не при каждом вызове.
def f(value, items=[]):
items.append(value)
return items
# Это происходит при определении функции:
# 1. Создаётся пустой список []
# 2. Этот список сохраняется как значение по умолчанию аргумента items
# 3. Этот ОДИН И ТОТ ЖЕ список используется во всех последующих вызовах
print(f.__defaults__) # ([1, 2, 3],) — список в памяти содержит [1, 2, 3]
Визуально:
Определение функции:
f(value, items=[]) → items указывает на один список в памяти
адрес памяти: 0x7f8b8c0a5000
Первый вызов f(1):
items указывает на 0x7f8b8c0a5000
items.append(1) → список становится [1]
Второй вызов f(2):
items ВСЁ ЕЩЁ указывает на 0x7f8b8c0a5000 (тот же список!)
items.append(2) → список становится [1, 2]
Третий вызов f(3):
items всё ещё 0x7f8b8c0a5000
items.append(3) → список становится [1, 2, 3]
3. Почему Python так делает?
Дефолтные значения вычисляются один раз при определении функции, а не при каждом вызове. Это оптимизация по производительности:
- Дорогие вычисления (например, подключение к БД) выполняются один раз
- Функция работает быстрее, так как значения уже вычислены
import time
def expensive_default(default=time.time()):
"""Используемое время вычисляется один раз."""
return default
print(expensive_default()) # 1679123456.789
time.sleep(1)
print(expensive_default()) # 1679123456.789 — ОДНО И ТО ЖЕ время!
4. Решение 1: Использование None (рекомендуется)
def f(value, items=None):
"""Правильный способ с изменяемым аргументом."""
if items is None:
items = []
items.append(value)
return items
print(f(1)) # [1]
print(f(2)) # [2]
print(f(3)) # [3]
Почему это работает?
None— неизменяемый объект, поэтому всегда одинаковый- На каждый вызов создаётся новый список
- Функция работает корректно
5. Решение 2: Копирование аргумента
def f(value, items=None):
"""Альтернатива: копируем переданный список."""
if items is None:
items = []
else:
items = items.copy() # Создаём копию для безопасности
items.append(value)
return items
print(f(1)) # [1]
print(f(2)) # [2]
# Если передать список явно, он не будет изменён
my_list = [10, 20]
print(f(3, my_list)) # [10, 20, 3]
print(my_list) # [10, 20] — не изменился!
6. Решение 3: Использование *args (для специальных случаев)
def f(value, *args):
"""Использование переменного количества аргументов."""
items = list(args) # Преобразуем кортеж в список
items.append(value)
return items
print(f(1)) # [1]
print(f(2)) # [2]
print(f(3)) # [3]
Когда использовать: Редко, обычно менее понятно, чем решение с None.
7. Правило для разных типов данных
# ❌ НЕПРАВИЛЬНО — для списков, словарей, множеств
def bad_list(item, items=[]):
items.append(item)
return items
def bad_dict(key, value, data={}):
data[key] = value
return data
def bad_set(item, items=set()):
items.add(item)
return items
# ✅ ПРАВИЛЬНО — используйте None
def good_list(item, items=None):
if items is None:
items = []
items.append(item)
return items
def good_dict(key, value, data=None):
if data is None:
data = {}
data[key] = value
return data
def good_set(item, items=None):
if items is None:
items = set()
items.add(item)
return items
# ✅ ПРАВИЛЬНО — неизменяемые типы можно использовать как есть
def good_string(item, prefix=""):
return prefix + str(item) # Строки неизменяемы
def good_number(item, multiplier=1):
return item * multiplier # Числа неизменяемы
def good_tuple(item, items=()):
return items + (item,) # Кортежи неизменяемы
8. Как обнаружить такую проблему в коде
# Проверка дефолтных значений функции
def f(value, items=[]):
items.append(value)
return items
print(f.__defaults__) # ([...],) — список как дефолт
print(type(f.__defaults__[0])) # <class 'list'>
# Проверка, изменяется ли дефолт
result1 = f(1)
result2 = f(2)
if result1 is f.__defaults__[0]:
print("ОПАСНО: используется общий список!")
9. Тестирование для защиты от ошибок
import unittest
class TestMutableDefaults(unittest.TestCase):
def test_correct_behavior_first_call(self):
def f(value, items=None):
if items is None:
items = []
items.append(value)
return items
result = f(1)
self.assertEqual(result, [1])
def test_correct_behavior_multiple_calls(self):
def f(value, items=None):
if items is None:
items = []
items.append(value)
return items
result1 = f(1)
result2 = f(2)
result3 = f(3)
self.assertEqual(result1, [1])
self.assertEqual(result2, [2])
self.assertEqual(result3, [3])
def test_wrong_behavior_exposed(self):
"""Демонстрирует проблему неправильного кода."""
def f_wrong(value, items=[]):
items.append(value)
return items
result1 = f_wrong(1)
result2 = f_wrong(2)
# Они указывают на ОДИН И ТОТ ЖЕ список в памяти
self.assertIs(result1, result2)
self.assertEqual(result2, [1, 2]) # Неожиданный результат!
10. Проверка на потенциальные проблемы (linting)
# flake8 расширение B006 ловит это:
# B006 Do not use mutable data structures for argument defaults
# mypy также может помочь при правильной аннотации:
def f(value: int, items: list[int] | None = None) -> list[int]:
if items is None:
items = []
items.append(value)
return items
Итоговые правила
Никогда не используйте как дефолт:
- ❌
[](список) - ❌
{}(словарь) - ❌
set()(множество) - ❌ Объекты ваших классов (если они изменяемы)
Вместо этого используйте:
- ✅
None+ проверка внутри функции - ✅ Неизменяемые типы:
(),"",0,False - ✅ Константы, которые не будут менять значение
Это одна из самых важных "gotchas" в Python, и её часто спрашивают на собеседованиях.