← Назад к вопросам
Почему не стоит делать изменяемый аргумент функции по умолчанию в Python?
2.0 Middle🔥 251 комментариев
#Python Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Мутируемые аргументы по умолчанию в Python: опасная ошибка
Это классическая ошибка новичков, которая вызывает странные баги. За 10+ лет я видел эту проблему в коде бесчисленное количество раз. Расскажу, почему это происходит и как это исправить.
Проблема: неправильное поведение
# ПЛОХО! Не делай так!
def add_item(item, items_list=[]):
items_list.append(item)
return items_list
# Первый вызов
result1 = add_item("apple")
print(result1) # ['apple']
# Второй вызов БЕЗ аргумента
result2 = add_item("banana")
print(result2) # ['apple', 'banana'] — ВОТ ЭТО НЕОЖИДАННО!
# Третий вызов
result3 = add_item("cherry")
print(result3) # ['apple', 'banana', 'cherry']
# А что если ты думаешь, что получишь новый список каждый раз?
list1 = add_item(1)
list2 = add_item(2)
list3 = add_item(3)
print(list1) # [1, 2, 3] — все три значения в первом списке!
print(list1 is list2) # True — это один и тот же объект!
Почему это происходит?
Причина: аргументы по умолчанию вычисляются один раз при определении функции, а не при каждом вызове.
# Это происходит один раз при определении функции
def problematic_function(items=[]):
# items указывает на конкретный объект в памяти
# Этот объект НЕ создаётся заново при каждом вызове
pass
# Наглядно:
def show_default(my_list=[]):
print(f"ID объекта: {id(my_list)}")
return my_list
print(id(show_default.__defaults__[0])) # 12345
show_default() # ID объекта: 12345
show_default() # ID объекта: 12345 — ТОТ ЖЕ ОБЪЕКТ!
Примеры мутируемых типов данных
# Опасные типы (изменяемые)
def bad_list(items=[]): # Опасно!
pass
def bad_dict(config={}): # Опасно!
pass
def bad_set(unique_items=set()): # Опасно!
pass
# Безопасные типы (неизменяемые)
def safe_string(text="default"): # OK
pass
def safe_number(count=0): # OK
pass
def safe_tuple(pair=()): # OK (но не используй как изменяемый)
pass
def safe_none(value=None): # Лучший вариант для мутируемых!
pass
Правильное решение: используй 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("apple")
print(result1) # ['apple']
# Второй вызов
result2 = add_item("banana")
print(result2) # ['banana'] — новый список!
# Третий вызов
result3 = add_item("cherry")
print(result3) # ['cherry'] — снова новый!
print(result1 is result2) # False — разные объекты!
Другой вариант: использование аннотаций типов
from typing import Optional, List
# Явно показывает, что используется mutable default
def process_data(data: Optional[List[str]] = None) -> List[str]:
if data is None:
data = []
return data
# Более современный подход с dataclass
from dataclasses import dataclass, field
@dataclass
class DataProcessor:
items: List[str] = field(default_factory=list)
def add_item(self, item: str) -> None:
self.items.append(item)
# Использование
processor1 = DataProcessor()
processor2 = DataProcessor()
processor1.add_item("apple")
print(processor1.items) # ['apple']
print(processor2.items) # [] — разные списки!
Ошибка с dict
# ПЛОХО!
def merge_configs(user_config, defaults={"debug": False}):
defaults.update(user_config)
return defaults
config1 = merge_configs({"timeout": 30})
config2 = merge_configs({"port": 8000})
print(config1) # {'debug': False, 'timeout': 30, 'port': 8000}
print(config2) # {'debug': False, 'timeout': 30, 'port': 8000}
# config1 и config2 — один и тот же объект!
# ХОРОШО!
def merge_configs(user_config, defaults=None):
if defaults is None:
defaults = {"debug": False}
result = defaults.copy() # Или используй copy.deepcopy()
result.update(user_config)
return result
Сложный пример: вложенные структуры
# Опасное поведение с вложенными данными
def build_tree(value, left=None, right=None):
return {
"value": value,
"left": left or [], # Опасно!
"right": right or []
}
tree1 = build_tree(1)
tree2 = build_tree(2)
tree1["left"].append("node")
print(tree2["left"]) # Может содержать "node" — BUG!
# Правильный способ
def build_tree_correct(value, left=None, right=None):
return {
"value": value,
"left": left if left is not None else [],
"right": right if right is not None else []
}
Инструменты для поиска таких ошибок
# Flake8 плагин
pip install flake8-bugbear
# Проверяет B006: мутируемые аргументы по умолчанию
flake8 --select=B006 script.py
# Mypy с правильными аннотациями найдёт проблемы
mypy --strict script.py
# Pylint тоже поддерживает обнаружение
pylint script.py
Чеклист для code review
# Спрашивай себя при code review:
# ❌ ПЛОХО
def func1(items=[]): # Список по умолчанию
pass
def func2(config={}): # Dict по умолчанию
pass
def func3(data=set()): # Set по умолчанию
pass
# ✅ ХОРОШО
def func1(items=None):
if items is None:
items = []
def func2(config=None):
if config is None:
config = {}
def func3(data=None):
if data is None:
data = set()
# АЛЬТЕРНАТИВНО ХОРОШО (Python 3.10+)
def func1(items=None):
items = items or []
def func2(config=None):
config = config or {}
Почему это важно?
- Предсказуемость: функция работает так, как ожидается
- Отсутствие побочных эффектов: одного вызова не влияют на другие
- Тестируемость: легче писать тесты
- Отладка: баги сразу видны, а не приводят к странному поведению через несколько часов
Итоговое правило: НИКОГДА не используй изменяемые объекты как аргументы по умолчанию. Используй None и создавай новый объект внутри функции. Это одна из самых распространённых ошибок в Python, которую нужно помнить.