Изменяемое значение по умолчанию
Представьте кафе, где посетителю без своей кружки выдают кружку заведения. Удобно: ничего не нужно приносить. Но кружка у кафе одна на всех, и её никто не моет. Второй посетитель получает её вместе с остатками чужого кофе.
Аргумент по умолчанию работает похоже. Он избавляет вызов от лишнего аргумента. Но если значение по умолчанию — список, функция начинает помнить прошлые вызовы:
def add_tag(tag, tags=[]):
tags.append(tag)
return tags
print(add_tag("red")) # ['red']
print(add_tag("blue")) # ['red', 'blue']
print(add_tag("new", [])) # ['new']Давайте разберём три вызова:
add_tag("red")не передаётtagsи получает общий список. Он пуст, туда попадает'red', и ответ верный.add_tag("blue")получает тот же общий список, где уже лежит'red'. Ответ —['red', 'blue'], хотя второй вызов ничего не знает о первом.add_tag("new", [])передаёт свой пустой список и получает верный ответ['new'].
Этот вопрос часто задают на собеседовании: «Что напечатает такая программа и почему?» Чтобы ответить, нужно знать, когда Python создаёт значение по умолчанию.
Когда создаётся значение
Значение по умолчанию вычисляется один раз — когда выполняется строка def, а не при каждом вызове. Python хранит получившийся объект вместе с функцией, и каждый вызов без аргумента tags получает этот же объект. Это и есть кружка заведения: её выдали один раз, когда кафе открылось.
Для числа или строки это незаметно: их нельзя изменить. Список изменить можно, поэтому append дописывает в один общий список, который живёт между вызовами. Первый вызов получает его пустым и отвечает верно, так что ошибка видна только со второго.
Третий вызов передал свой список, и параметр tags стал именем для нового []. Общий список этот вызов не тронул: в нём по-прежнему ['red', 'blue'].
Здесь аналогия с кафе заканчивается: в кафе грязную кружку хотя бы видно. В Python общий список спрятан внутри функции, и снаружи вызовы выглядят независимыми.
Значение None
Чтобы каждый вызов получал новый список, значением по умолчанию делают None, а сам список создают в теле функции. В кафе это значит: гостю без своей кружки каждый раз достают новую, чистую.
def add_tag(tag, tags=None):
if tags is None:
tags = []
tags.append(tag)
return tags
print(add_tag("red")) # ['red']
print(add_tag("blue")) # ['blue']Разберём тело функции:
if tags is None:проверяет, передан ли аргумент.tags = []выполняется при каждом вызове безtagsи каждый раз создаёт новый список.- Дальше
tagsвсегда ссылается на список: либо переданный, либо только что созданный.
None здесь служит меткой — знаком того, что аргумент не передан. Проверка is None отличает такой вызов от вызова с пустым списком. Так поступают с любым изменяемым значением по умолчанию, не только со списком.
Частая ошибка
Проверку is None заменили на or:
def add_tag(tag, tags=None):
tags = tags or [] # ✗
tags.append(tag)
return tags
mine = []
add_tag("red", mine)
print(mine) # []Оператор or проверяет истинность, а не то, передан ли аргумент. Пустой список mine ложный, поэтому tags or [] даёт новый список, и "red" попадает в него, а mine остаётся пустым. Такая функция работает верно, пока вызывающий код не передаст пустой список. С проверкой if tags is None: функция дописала бы "red" в mine.
Главное
- Значение по умолчанию вычисляется один раз, при выполнении
def, и общее для всех вызовов. - Список в значении по умолчанию копит данные между вызовами.
- Значением по умолчанию делают
None, а новый список создают в теле функции после проверкиis None. tags or []подменяет и переданный пустой список, поэтому нужна проверкаis None.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.