← Назад к вопросам

Проблема изменяемого аргумента по умолчанию

2.0 Middle🔥 201 комментариев
#Python Core

Условие

Что выведет следующий код и почему?

def f(value, items=[]):
    items.append(value)
    return items

print(f(1))
print(f(2))
print(f(3))

Задача

  1. Объясните поведение
  2. Исправьте функцию так, чтобы она работала корректно

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Проблема изменяемого аргумента по умолчанию

Это одна из самых коварных ошибок в 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, и её часто спрашивают на собеседованиях.

Проблема изменяемого аргумента по умолчанию | PrepBro