Изменяемые аргументы по умолчанию в Python: почему это ошибка и как делать правильно
Запрос «изменяемые аргументы по умолчанию в Python» регулярно появляется в поиске — и не зря. Это одна из самых коварных ошибок: вы пишете безобидную функцию со списком по умолчанию, а потом внезапно данные «накапливаются» между вызовами. Разберёмся на примерах и закрепим правильные паттерны.
Почему это происходит: значения по умолчанию вычисляются один раз
В Python значения по умолчанию вычисляются в момент определения функции, а не при каждом вызове. Поэтому если вы используете изменяемый объект (list, dict, set), он будет общий для всех последующих вызовов без явной передачи параметра.
def append_item(item, bucket=[]):
bucket.append(item)
return bucket
print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] — <!> неожиданно!
print(append_item(3, [])) # [3] — если передать новый список, всё ок
# Покажем, что дефолт «живёт» в функции
print(append_item.__defaults__) # (['1', '2'],) — здесь хранится тот самый список
Аналогичный эффект проявляется и в конструкторах классов:
class User:
def __init__(self, name, tags=[]):
self.name = name
self.tags = tags
u1 = User("Ann")
u2 = User("Bob")
u1.tags.append("admin")
print(u2.tags) # ['admin'] — второй пользователь «унаследовал» тег первого
Правильный паттерн: используем None как «сентинел»
Стоимость проверки None ничтожна, а читаемость и надёжность — на высоте. Это канонический способ.
def append_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
print(append_item(1)) # [1]
print(append_item(2)) # [2] — новый список при каждом вызове
print(append_item(3, [10])) # [10, 3] — используем свой список
То же для словарей и множеств:
def add_flag(key, flags=None):
if flags is None:
flags = {}
flags[key] = True
return flags
print(add_flag("x")) # {'x': True}
print(add_flag("y")) # {'y': True}
Dataclasses: используйте default_factory
В dataclass нельзя ставить изменяемые значения напрямую: используйте field(default_factory=...). Это создаёт новый объект при каждом создании экземпляра.
from dataclasses import dataclass, field
@dataclass
class User:
name: str
tags: list[str] = field(default_factory=list)
u1 = User("Ann")
u2 = User("Bob")
u1.tags.append("admin")
print(u2.tags) # [] — теги независимы
Аннотации типов и None
Если используете аннотации, помните про None в типах. Для Python 3.10+ подойдёт оператор |.
def process(items: list[int] | None = None) -> list[int]:
if items is None:
return []
return [x * 2 for x in items]
В более ранних версиях:
from typing import Optional, List
def process(items: Optional[List[int]] = None) -> List[int]:
return [] if items is None else [x * 2 for x in items]
Не только списки: вызовы в дефолтах тоже опасны
Любая функция в дефолте выполнится при определении, а не при вызове. Пример с датой:
from datetime import datetime
def log(ts=datetime.now()): # <!> ВСЕ вызовы получат один и тот же момент времени
return ts.isoformat()
Правильно так:
from datetime import datetime
def log(ts=None):
if ts is None:
ts = datetime.now()
return ts.isoformat()
Когда «шэрить» дефолт можно осознанно
Иногда вам действительно нужен общий «кеш» между вызовами. Но делайте это явно — через замыкание, глобальную переменную или декоратор кеширования, чтобы код было легче читать и тестировать.
# Ясный и управляемый стейт через замыкание
def make_counter(start=0):
value = start
def inc():
nonlocal value
value += 1
return value
return inc
c = make_counter()
print(c()) # 1
print(c()) # 2
Если нужен мемо-кеш по аргументам — лучше используйте functools.lru_cache.
from functools import lru_cache
@lru_cache(maxsize=1024)
def fib(n: int) -> int:
if n < 2:
return n
return fib(n-1) + fib(n-2)
Как ловить проблему заранее
- Линтеры: Pylint (W0102: dangerous default value), flake8-bugbear (B006 для изменяемых дефолтов, B008 для вызовов в дефолтах).
- Код-ревью: проверяйте сигнатуры на list/dict/set/{} или вызовы функций.
- Тесты: делайте два последовательных вызова без передачи аргумента и сравнивайте результаты.
def test_append_item_is_fresh():
assert append_item(1) == [1]
assert append_item(2) == [2]
Чеклист перед коммитом
- Нет ли в параметрах list/dict/set как значений по умолчанию?
- Нет ли вызовов функций в дефолтах (datetime.now(), uuid4(), Path.cwd())?
- В dataclass для изменяемых полей стоит field(default_factory=...)?
- С None-сентинелем покрыты все ветки тестами?
Итоги
Главное правило: не используйте изменяемые аргументы по умолчанию в Python. Ставьте None и инициализируйте внутри функции, а в dataclass применяйте default_factory. Исключения бывают, но должны быть намеренными и явно оформленными. Следуя этим простым принципам, вы избежите множества неприятных и трудноуловимых багов.
Хотите системно прокачать навыки и научиться писать надёжный продакшен-код? Рекомендую пройти практический курс «Python с Нуля до Гуру» — много реальных задач, разбор типичных ошибок и менторская поддержка.
-
Создано 20.03.2026 17:01:56
-
Михаил Русаков

Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.