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

Почему изменяемые типы нельзя делать дефолтными аргументами функции?

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

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

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

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

Почему изменяемые типы нельзя делать дефолтными аргументами функции?

Это одна из самых известных "ловушек" Python. Проблема в том, что дефолтные аргументы вычисляются один раз — при определении функции, а не при каждом вызове.

Проблема: Mutable Default Arguments

Неправильный код:

def add_item(item, items_list=[]):
    items_list.append(item)
    return items_list

result1 = add_item(1)
print(result1)  # [1]

result2 = add_item(2)
print(result2)  # [1, 2] — ВОТ ЗДЕСЬ ПРОБЛЕМА!

result3 = add_item(3)
print(result3)  # [1, 2, 3] — Список растёт!

Ждем [2], получаем [1, 2]. Почему?

Объяснение механизма

Дефолтные аргументы функции создаются один раз при определении функции:

def add_item(item, items_list=[]):
    print(id(items_list))  # ID одного и того же объекта!
    items_list.append(item)
    return items_list

add_item(1)
add_item(2)
add_item(3)

Вывод (обрати внимание на ID):

140329743456000  # Один и тот же объект
140329743456000
140329743456000

Все вызовы функции используют ОДИН И ТОТ ЖЕ список!

Как Python обрабатывает дефолтные аргументы

def func(arg=default_value):
    pass

# Эквивалент:
def func(arg=None):
    pass

func.__defaults__  # Кортеж дефолтных значений
# Создаётся один раз при определении

Проверим на примере:

def problematic(items=[]):
    items.append(1)
    return items

# Смотрим на __defaults__
print(problematic.__defaults__)  # ([1],) — уже есть элемент!

# Список в __defaults__ — реальный объект, не копия
print(id(problematic.__defaults__[0]))  # 140329743456000

Почему только изменяемые типы опасны?

Неизменяемые типы (int, str, tuple) — безопасны:

def safe_func(x=5, name="default"):
    x = x + 1
    name = name + "_modified"
    return x, name

print(safe_func())  # (6, 'default_modified')
print(safe_func())  # (6, 'default_modified') — всегда одинаково

Почему это работает?

def safe_func(x=5):
    x = x + 1  # Создаёт НОВЫЙ объект (5 is immutable)
    return x

Мы не изменяем исходный объект, создаём новый.

Изменяемые типы (list, dict, set) — опасны:

def dangerous(items=[]):
    items.append(1)  # Модифицирует ИСХОДНЫЙ объект
    return items

Мы изменяем сам объект, а не создаём новый.

Правильная реализация

Вариант 1: None в качестве дефолта

def add_item(item, items_list=None):
    if items_list is None:
        items_list = []
    items_list.append(item)
    return items_list

result1 = add_item(1)
print(result1)  # [1]

result2 = add_item(2)
print(result2)  # [2] — Правильно!

result3 = add_item(3)
print(result3)  # [3]

# Можно передать свой список
my_list = [10]
result4 = add_item(4, my_list)
print(result4)  # [10, 4]

Вариант 2: Factory функция (вызвать при создании)

def add_item(item, items_list=None):
    if items_list is None:
        items_list = []  # Создаётся НОВЫЙ список при каждом вызове
    items_list.append(item)
    return items_list

Тот же результат, но понятнее намерение.

Проблема с другими изменяемыми типами

Словарь:

# ❌ Плохо
def update_config(key, value, config={}):
    config[key] = value
    return config

update_config("a", 1)
update_config("b", 2)
print(update_config("c", 3))  # {'a': 1, 'b': 2, 'c': 3} — все ключи!

# ✅ Хорошо
def update_config(key, value, config=None):
    if config is None:
        config = {}
    config[key] = value
    return config

Set:

# ❌ Плохо
def add_tags(tag, tags=set()):
    tags.add(tag)
    return tags

add_tags("python")
add_tags("javascript")
print(add_tags("rust"))  # {"python", "javascript", "rust"}

# ✅ Хорошо
def add_tags(tag, tags=None):
    if tags is None:
        tags = set()
    tags.add(tag)
    return tags

Реальный пример из практики

# ❌ Ошибка в реальном коде
def create_user(name, roles=[]):
    user = {"name": name, "roles": roles}
    return user

user1 = create_user("Alice", ["admin"])
user2 = create_user("Bob")  # Bob получит роли Alice!
user2["roles"].append("user")
user3 = create_user("Charlie")  # Charlie тоже admin!

# ✅ Правильно
def create_user(name, roles=None):
    if roles is None:
        roles = []
    user = {"name": name, "roles": roles}
    return user

Проверка на практике

def problematic(lst=[]):
    print(f"Список ID: {id(lst)}")
    print(f"Содержимое: {lst}")
    lst.append(1)
    print()

problematic()  # ID: 140329743456000, []
problematic()  # ID: 140329743456000, [1] — ОДИН ТОТ ЖЕ ОБЪЕКТ!
problematic()  # ID: 140329743456000, [1, 1]

print(problematic.__defaults__)  # ([1, 1],) — Изменилось!

Почему так проектировано?

Это решение для производительности:

# Дефолты вычисляются один раз
def func(arg=expensive_calculation()):
    pass

# Вместо вычисления при каждом вызове

Но это создаёт опасность с mutable объектами.

Чеклист

  • def func(items=[]): ... — ОПАСНО
  • def func(config={}): ... — ОПАСНО
  • def func(data=set()): ... — ОПАСНО
  • def func(items=None): ... — ПРАВИЛЬНО
  • def func(x=5): ... — БЕЗОПАСНО (неизменяемый тип)
  • def func(name="default"): ... — БЕЗОПАСНО (неизменяемый тип)

Вывод: Никогда не используй изменяемые типы как дефолтные аргументы. Используй None в качестве дефолта и создавай новый объект внутри функции.

Почему изменяемые типы нельзя делать дефолтными аргументами функции? | PrepBro