Рассмотрите следующий фрагмент на Python:
def add_to_list(x, lst=[]):
lst.append(x)
return lst
Объясните семантику этого кода, почему он ведёт себя неожиданно при последовательных вызовах, какие фундаментальные концепции языка здесь задействованы, и предложите корректные альтернативы (с объяснением преимуществ и недостатков каждого варианта)?

12 Окт в 14:14
2 +1
0
Ответы
1

Код
def add_to_listx,lst=[]x, lst=[]x,lst=[]:
lst.appendxxx return lst
имеет семантику, которую часто считают «неожиданной», но она полностью соответствует правилам Python. Вот что важно знать и почему так происходит.

Что происходит короткокороткокоротко

Значение параметра по умолчанию lst = вычисляется один раз — в момент определения функции когдавыполняетсяоператорdefкогда выполняется оператор defкогдавыполняетсяоператорdef, а не при каждом вызове.В результате каждый раз, когда вы вызываете add_to_list…… без явного второго аргумента, используется один и тот же объект списка. Поскольку список изменяем mutablemutablemutable, изменения appendappendappend сохраняются между вызовами.

Пример поведения

add_to_list111 111 add_to_list222 1,21, 21,2 add_to_list3,[]3, []3,[] 333 # передали новый список — всё нормально
add_to_list444 1,2,41, 2, 41,2,4 # продолжает накапливаться в одном списке по умолчанию

Фундаментальные концепции языка, задействованные здесь

Значения параметров по умолчанию вычисляются один раз при определении функции см.спецификациюPythonсм. спецификацию Pythonсм.спецификациюPython.В Python имена в переменных ссылаются на объекты; списки — изменяемые объекты. Когда вы изменяете список, вы меняете сам объект, на который ссылаются все ссылки.Следствие: если дефолтный объект изменяемый, вызовы без параметра будут видеть эти изменения разделяемоесостояниеразделяемое состояниеразделяемоесостояние.Дополнительно: этот дефолтный объект хранится в атрибуте функции defaults и живёт, пока живёт объект функции т.е.небудетудалёнсборщикомт.е. не будет удалён сборщикомт.е.небудетудалёнсборщиком.

Когда такое поведение может быть желательным

Если вы намеренно хотите сохранить состояние между вызовами функции например,кеширование,накоплениенапример, кеширование, накоплениенапример,кеширование,накопление, то использование изменяемого дефолта — допустимый приём. Но это должно быть явным и задокументированным.

Корректные альтернативы собъяснениемплюс/минусс объяснением плюс/минуссобъяснениемплюс/минус

1) Сентинел None рекомендуемыйисамыйраспространённыйрекомендуемый и самый распространённыйрекомендуемыйисамыйраспространённый def add_to_listx,lst=Nonex, lst=Nonex,lst=None:
if lst is None:
lst = lst.appendxxx return lst

Преимущества:

Проста и понятна.Не разделяет состояние между вызовами.Работает корректно, когда ожидается «новый пустой список» по умолчанию.

Недостатки:

Если вы хотите позволить явно передавать None как валидный аргумент, этот приём не подходит нообычноэтоненужноно обычно это не нужнонообычноэтоненужно.

Важное замечание: не стоит писать lst = lst or — это неправильно, если допустимым и значимым аргументом является пустой список пустойсписок—falsyпустой список — falsyпустойсписокfalsy, такой код создаст новый список и перезапишет переданный пустой список.

2) Использовать уникальный объект-сентинел
_SENTINEL = object def add_to_listx,lst=SENTINELx, lst=_SENTINELx,lst=S ENTINEL:
if lst is _SENTINEL:
lst = lst.appendxxx return lst

Преимущества:

Явно отличает «параметр не передан» от «передан None».Более формально и безопасно в редких случаях, когда None тоже может быть валидным входом.

Недостатки:

Немного более многословно.

3) Возвращать новый список функциональныйстиль,безизменениявходногофункциональный стиль, без изменения входногофункциональныйстиль,безизменениявходного def add_to_listx,lst=Nonex, lst=Nonex,lst=None:
if lst is None:
lst = new = lst + xxx # или new = listlstlstlst; new.appendxxx return new

Преимущества:

Нет побочных эффектов на входной список.Подходит для иммутабильного/функционального стиля программирования.

Недостатки:

Создаётся копия списка при каждом вызове — накладные расходы по времени и памяти.

4) Передача фабрики режережереже def add_to_listx,factory=listx, factory=listx,factory=list:
lst = factory lst.appendxxx return lst

Преимущества:

Гибкость: можно передать кастомный класс/фабрику.

Недостатки:

Немного необычно в API функции, и фабрика вызывается всегда еслинужнотолькоприотсутствииаргумента—комбинируйтесNoneесли нужно только при отсутствии аргумента — комбинируйте с NoneеслинужнотолькоприотсутствииаргументакомбинируйтесNone.

5) Если вы действительно хотите сохранять состояние между вызовами — используйте это сознательно
def counterval,storage=val, storage={}val,storage=:
storage′sum′'sum'sum = storage.get′sum′,0'sum', 0sum,0 + val
return storage′sum′'sum'sum

Преимущества:

Удобный приём для кэша/накопления.

Недостатки:

Побочные эффекты, может путать пользователей функции, проблемы для многопоточности, тестируемости.

Дополнительные замечания

Можно посмотреть значение дефолтов через add_to_list.defaults — там хранится кортеж дефолтных объектов.Использование изменяемого дефолта удерживает объект в памяти покаживётфункцияпока живёт функцияпокаживётфункция, что иногда влияет на жизненный цикл объектов.В многопоточном окружении общий дефолтный объект — источник гонок, требуется синхронизация.

Краткий вывод
Поведение функции обусловлено тем, что дефолтные значения вычисляются один раз при определении функции, а списки — изменяемые объекты. Если вы не хотите, чтобы состояние накапливалось между вызовами, используйте паттерн «None-сентинел + создание нового списка внутри функции» илиуникальныйобъект−сентинелили уникальный объект-сентинелилиуникальныйобъектсентинел, либо возвращайте новую копию списка. Если же вы хотите аккумулировать состояние — используйте изменяемый дефолт осознанно и документируйте это.

12 Окт в 14:52
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир