Рассмотрите фрагмент на Python: def add_item(item, lst=[]): lst.append(item); return lst; print(add_item(1)); print(add_item(2)); — объясните, почему поведение может отличаться от ожиданий, как это исправить и чем отличие от языков с семантикой "по значению" (например, Java, где параметры по умолчанию отсутствуют) влияет на дизайн API
Коротко — потому что в Python значение параметра по умолчанию вычисляется один раз при объявлении функции и потом переиспользуется. В вашем примере список по умолчанию один и тот же для всех вызовов, поэтому второй вызов добавляет элемент в уже существующий список. Пример поведения: - print(add_item( 111 )) выводит [1][1][1]
- print(add_item( 222 )) выводит [1,2][1,2][1,2] Как исправить (идемпотентный шаблон с None): def add_item(item, lst=None): if lst is None: lst = [] lst.append(item) return lst Теперь: - print(add_item( 111 )) → [1][1][1]
- print(add_item( 222 )) → [2][2][2] Альтернативы: использовать неизменяемый тип как дефолт (например кортеж) или явный sentinel-объект. Чем это отличается от языков «по значению» / без дефолтных параметров (например Java) и как это влияет на дизайн API: - В Java дефолтных параметров нет, поэтому типичная замена — перегрузки методов или билдеры; проблема «скрытого повторного использования» дефолта просто не возникает. - В Java объекты передаются как копии ссылок (pass-by-value-of-reference): если вы явно передали один и тот же изменяемый объект, он тоже будет общим, но это явный вызов, а не скрытое поведение дефолтов. - Следствие для дизайна API: в языках с дефолтами (Python) нужно быть осторожным с изменяемыми дефолтами — лучше использовать None/сентинел или immutable-значения; API должен документировать, делается ли копия аргумента или объект мутируется. В языках без дефолтов проблемы смещаются в сторону явных перегрузок и требований к копированию/иммутабельности.
Пример поведения:
- print(add_item( 111 )) выводит [1][1][1] - print(add_item( 222 )) выводит [1,2][1,2][1,2]
Как исправить (идемпотентный шаблон с None):
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
Теперь:
- print(add_item( 111 )) → [1][1][1] - print(add_item( 222 )) → [2][2][2]
Альтернативы: использовать неизменяемый тип как дефолт (например кортеж) или явный sentinel-объект.
Чем это отличается от языков «по значению» / без дефолтных параметров (например Java) и как это влияет на дизайн API:
- В Java дефолтных параметров нет, поэтому типичная замена — перегрузки методов или билдеры; проблема «скрытого повторного использования» дефолта просто не возникает.
- В Java объекты передаются как копии ссылок (pass-by-value-of-reference): если вы явно передали один и тот же изменяемый объект, он тоже будет общим, но это явный вызов, а не скрытое поведение дефолтов.
- Следствие для дизайна API: в языках с дефолтами (Python) нужно быть осторожным с изменяемыми дефолтами — лучше использовать None/сентинел или immutable-значения; API должен документировать, делается ли копия аргумента или объект мутируется. В языках без дефолтов проблемы смещаются в сторону явных перегрузок и требований к копированию/иммутабельности.