Почему изменяемые типы нельзя делать дефолтными аргументами функции?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему изменяемые типы нельзя делать дефолтными аргументами функции?
Это одна из самых известных "ловушек" 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 в качестве дефолта и создавай новый объект внутри функции.