List astonishment
Допустим, мы хотим написать функцию, которая будет принимать на вход список. Естественным ходом выглядит написать что-то вроде:
>>> def foo(data=[]):
... data.append(5)
... return data
Ждем, что при каждом новом вызовы функции список снова будет пустым. На самом же деле:
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
Вот это поворот! Причина такого поведения в том, что в Python значения дефолтных аргументов вычисляются только один раз -- при объявлении функции. То есть, после того, как инструкция def выполнена, список data уже создан. При этом пока функция существует, пересоздаваться список больше не будет. Это объясняет и вот такое поведение:
>>> foo(data=[1, 2, 3])
[1, 2, 3, 5]
>>> foo()
[5, 5, 5]
Видно, что, когда мы передали другой список в foo(), к нему добавилось значение 5. При этом аргумент по умолчанию не изменился.
Как с этим жить? Либо вообще не использовать изменяемые объекты в качестве дефолтных значний, либо инициализировать их как None:
>>> def bar(data=None):
... if data is None:
... data = []
... data.append(5)
... return data
Такое определение создает новый список в локальном неймспейсе. Теперь все чисто😏
>>> bar()
[5]
>>> bar()
[5]
>>> bar(data=[1, 2, 3])
[1, 2, 3, 5]
На самом деле это поведение можно обратить в свою пользу. Пусть, например, в программе есть функция, которая долго вычисляет результат. Можно кешировать параметры и результат вычисления, и, если функцию вызывают еще раз с теми же парметрами, просто возвращать заранее посчитанный ответ.
def baz(arg1, arg2, _cache={}):
# Если есть в кеше, выходим
if (arg1, arg2) in _cache:
return _cache[(arg1, arg2)]
# Вычисляем результат
result = ...
# Сохраняем в кеш
_cache[(arg1, arg2)] = result
return result
Сохранение результатов для предотвращения повторных вычислений называется мемоизацией.
В Python дефолтные аргументы инициализируются только при объявлении функции. Это значит, что изменения, которые делаются в мутабельных аргументах при вызове функции, сохраняются. Поэтому лучше не использовать списки, словари и экземпляры самописных классов как значения по умолчанию. Либо использовать специально, как кеш, в котором можно хранить полезную информацию🐍
Закончу цитатой:
Не повторяйте одну и ту же ошибку несколько раз. Напишите функцию с этой ошибкой и вызывайте, когда будет нужно.
#функции #мемоизация #кеширование #мутабельность #иммутабельность
#python