В коде Python обнаружен следующий баг: "def append_item(lst=[]): lst.append(1); return lst". Объясните причину неожиданного поведения при множественных вызовах, перечислите три способа исправления и сравните их влияние на читаемость, производительность и совместимость с аннотациями типов
В 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 (но учтите неудобства с аннотациями).Если вы не хотите мутировать переданный список и предпочитаете чистые функции, возвращайте новый список — это безопаснее но может потребовать копирования и поэтому дороже по времени/памяти для больших списков.
Коротко о причине
В 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 (но учтите неудобства с аннотациями).Если вы не хотите мутировать переданный список и предпочитаете чистые функции, возвращайте новый список — это безопаснее но может потребовать копирования и поэтому дороже по времени/памяти для больших списков.