Изменяемы типы в качестве параметров по умолчанию в Python

Опубликовано 11 April 2016 в Python

Почему изменяемые типы не рекомендуется использовать в качестве значений аргументов по умолчанию в Python? Если вы забрались по карьерной лестнице выше джуниора, то наверняка задумывались. И наверняка ответ был таким: "это приводит к странным побочным эффектам". Но я почти наверняка уверен, что только малая часть разработчиков на питоне сделала шаг дальше и разобралась почему такое поведение у языка.

Давайте разберемся сначала с самим какими побочными эффектами имеем дело. Напишем вот такую функцию:

def foo(param=[]):
    print(param)

>>> foo()
[]

Сколько ее не вызывай, значение выводимое на экран не изменится. Никаких побочных эффектов. Но если ее модифицировать вот так:

def foo(param=[]):
    param.append(1)
    print(param)

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

>>> foo()
[1]
>>> foo()
[1,1]
>>> foo()
[1,1,1]

Но мы ведь хотели не этого. Такое поведение функций - один из лидеров разного рода списков "Х самых странных вещей в Python". Хотя, если разобраться с внутренней кухней языка, то все встанет на свои места и "странность" превратится в логичное поведение.

Почему же так получилось? Все из-за того, что функция - это объект, как и все другие сущности в питоне. При создании объекта дефолтные значения параметров упаковались в tuple foo.func_defaults.

Списки в питоне - тип изменяемый. А объект функции создается один раз. Так что объект на который ссылается foo.func_defaults[0] и param внутри функции один и тот же.

def foo(param=[]):
    print(id(param))

>>> foo()
4494207600
>>> id(foo.func_defaults[0])
4494207600

Правильным будет передача None в качестве значения по умолчанию. А в теле функции уже инициализировать изменяемый объект. А подсказку по типу параметра прописывать в докстринге или, если используется Python 3.5+, то использовать PEP 0484.

def foo(param=None):
    """
    :type param: list
    """
    param = [] if param is None else param
    param.append(1)
    print(param)

>>> foo()
[1]
>>> foo()
[1]

Для коротких функций в 5-6 строк добавление проверки на None - замусоривание кода. В этих случаях нужно хорошо подумать оставить эту проверку и обезопасить код или убрать ее. Я за безопасность. Потом делая рефакторинг можно пропустить изменения, которые внесут побочные эффекты.

---
Возник вопрос? Мне всегда можно написать в Twitter: avkorablev