В коде Python обнаружен следующий баг: "def append_item(lst=[]): lst.append(1); return lst". Объясните причину неожиданного поведения при множественных вызовах, перечислите три способа исправления и сравните их влияние на читаемость, производительность и совместимость с аннотациями типов

30 Сен в 17:26
12 +1
0
Ответы
1

Коротко о причине

В Python значение аргумента по умолчанию вычисляется один раз при определении функции. В вашем примере lst = [] создаётся единожды и тот же список переиспользуется при каждом вызове без явного аргумента. Поэтому последовательные вызовы накапливают значения в одном и том же списке:
Первый вызов append_item() вернёт [1]Второй вызов append_item() вернёт [1, 1]
и т.д.

Три способа исправления (с примерами и комментариями)

1) Идиоматичный способ: default = None, создать список внутри
Код:
def append_item(lst: Optional[list[int]] = None) -> list[int]:
if lst is None:
lst = []
lst.append(1)
return lst

Комментарий

Читаемость: высокая — стандартный и широко используемый паттерн.Производительность: проверка на None и выделение списка при каждом вызове — очень дешёвая операция; при обычном использовании накладные расходы несущественны.Совместимость с аннотациями: хорошая — нужно аннотировать как Optional[list[int]] (или Union[list[int], None]) и возвращаемый тип list[int].

2) Sentinel-объект (если None допустимо как валидный аргумент)
Код:
_MISSING = object()
def append_item(lst=_MISSING):
if lst is _MISSING:
lst = []
lst.append(1)
return lst

Комментарий

Читаемость: чуть хуже, чем None-паттерн; разработчику нужно понять назначение sentinel.Производительность: аналогична None-версии.Совместимость с аннотациями: хуже — если аннотировать lst как list[int] -> mypy/проверяющие типы не понравятся default=_MISSING. Обычно приходится аннотировать как Optional[list[int]] либо как Any, или использовать overloads. Поэтому sentinel чаще применяется, когда None — валидный пользовательский аргумент и нужно отличать «не передано» от «передано None».

3) Не мутировать аргумент — возвращать новый список (функциональный стиль)
Код:
from typing import Optional, Sequence

def append_item(lst: Optional[Sequence[int]] = None) -> list[int]:
base = list(lst) if lst is not None else []
base.append(1)
return base

(альтернатива короткая: return (list(lst) if lst is not None else []) + [1])

Комментарий

Читаемость: хорошая; явное поведение — функция возвращает новый список, не меняя входной.Производительность: немного хуже, если входной список большой — приходится копировать список (list(...)) или создавать новый через конкатенацию; если списки маленькие, накладные расходы невелики.Совместимость с аннотациями: хорошая — можно принимать Sequence[int] (или Optional[Sequence[int]]) и возвращать list[int]. Полезно, если вы хотите избежать побочных эффектов (мутаций) в вызывающем коде.

Резюме и рекомендация

Самый распространённый и простой исправляющий паттерн — default=None и создание списка внутри функции. Он хорошо читается, совместим с типизацией (Optional[list[T]]) и имеет минимальные накладные расходы.Если None — валидное значение аргумента и нужно отличать «не передано» от «передано None», используйте sentinel (но учтите неудобства с аннотациями).Если вы не хотите мутировать переданный список и предпочитаете чистые функции, возвращайте новый список — это безопаснее но может потребовать копирования и поэтому дороже по времени/памяти для больших списков.
30 Сен в 17:58
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир