Перейти к содержимому
Теория · Списки · урок 8 из 11

Изменяемое значение по умолчанию

Представьте кафе, где посетителю без своей кружки выдают кружку заведения. Удобно: ничего не нужно приносить. Но кружка у кафе одна на всех, и её никто не моет. Второй посетитель получает её вместе с остатками чужого кофе.

Аргумент по умолчанию работает похоже. Он избавляет вызов от лишнего аргумента. Но если значение по умолчанию — список, функция начинает помнить прошлые вызовы:

Python
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']

Давайте разберём три вызова:

  1. add_tag("red") не передаёт tags и получает общий список. Он пуст, туда попадает 'red', и ответ верный.
  2. add_tag("blue") получает тот же общий список, где уже лежит 'red'. Ответ — ['red', 'blue'], хотя второй вызов ничего не знает о первом.
  3. add_tag("new", []) передаёт свой пустой список и получает верный ответ ['new'].

Этот вопрос часто задают на собеседовании: «Что напечатает такая программа и почему?» Чтобы ответить, нужно знать, когда Python создаёт значение по умолчанию.

Когда создаётся значение

Значение по умолчанию вычисляется один раз — когда выполняется строка def, а не при каждом вызове. Python хранит получившийся объект вместе с функцией, и каждый вызов без аргумента tags получает этот же объект. Это и есть кружка заведения: её выдали один раз, когда кафе открылось.

Для числа или строки это незаметно: их нельзя изменить. Список изменить можно, поэтому append дописывает в один общий список, который живёт между вызовами. Первый вызов получает его пустым и отвечает верно, так что ошибка видна только со второго.

Третий вызов передал свой список, и параметр tags стал именем для нового []. Общий список этот вызов не тронул: в нём по-прежнему ['red', 'blue'].

Здесь аналогия с кафе заканчивается: в кафе грязную кружку хотя бы видно. В Python общий список спрятан внутри функции, и снаружи вызовы выглядят независимыми.

Значение None

Чтобы каждый вызов получал новый список, значением по умолчанию делают None, а сам список создают в теле функции. В кафе это значит: гостю без своей кружки каждый раз достают новую, чистую.

Python
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']

Разберём тело функции:

  1. if tags is None: проверяет, передан ли аргумент.
  2. tags = [] выполняется при каждом вызове без tags и каждый раз создаёт новый список.
  3. Дальше tags всегда ссылается на список: либо переданный, либо только что созданный.

None здесь служит меткой — знаком того, что аргумент не передан. Проверка is None отличает такой вызов от вызова с пустым списком. Так поступают с любым изменяемым значением по умолчанию, не только со списком.

Частая ошибка

Проверку is None заменили на or:

Python
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 и подпишись на канал — откроются все уроки.

Войти другим способом