Изменяемые значения параметров по умолчанию в Python
Обновлено:
Мы уже знаем, что функции могут иметь параметры со значениями по умолчанию:
def calculate_price(price, tax=0.2):
return price + price * tax
Но с изменяемыми объектами, например со списками, появляется важная особенность.
1. Список как значение по умолчанию
На первый взгляд такой код выглядит нормально:
def add_product(product, products=[]):
products.append(product)
return products
Вызовем функцию несколько раз:
print(
add_product('Laptop')
)
print(
add_product('Mouse')
)
print(
add_product('Keyboard')
)
Результат:
['Laptop']
['Laptop', 'Mouse']
['Laptop', 'Mouse', 'Keyboard']
При каждом новом вызове мы могли ожидать новый пустой список.
Но этого не произошло.
Почему так происходит
2. Значение по умолчанию создаётся один раз
Когда Python выполняет определение функции:
def add_product(product, products=[]):
...
выражение:
[]
вычисляется один раз.
Создаётся один объект списка, который затем сохраняется как значение параметра по умолчанию.
Упрощённо:
создание функции
↓
создаётся []
↓
этот список сохраняется как значение по умолчанию
↓
первый вызов использует этот список
↓
второй вызов использует тот же список
↓
третий вызов использует тот же список
Поэтому:
products.append(product)
каждый раз изменяет один и тот же объект.
3. Это можно увидеть по id()
def add_number(number, numbers=[]):
print(id(numbers))
numbers.append(number)
return numbers
Вызовем:
add_number(10)
add_number(20)
add_number(30)
При обычной работе программы во всех трёх вызовах параметр numbers будет ссылаться на тот же объект списка.
Именно поэтому его содержимое накапливается.
Почему с int такой проблемы нет
4. Неизменяемые объекты
Например:
def increase(number=0):
number += 1
return number
Вызовем:
print(increase())
print(increase())
print(increase())
Результат:
1
1
1
int является неизменяемым объектом.
Операция:
number += 1
не изменяет существующий объект 0, а связывает локальное имя number с новым объектом.
Поэтому проблема особенно важна для изменяемых объектов:
list
dict
set
экземпляры некоторых собственных классов
С некоторыми из этих типов познакомимся позже.
Правильный вариант
5. Используем None
Если при каждом вызове функции нужен новый список, стандартный подход:
def add_product(
product,
products=None,
):
if products is None:
products = []
products.append(product)
return products
Теперь:
print(
add_product('Laptop')
)
print(
add_product('Mouse')
)
print(
add_product('Keyboard')
)
Результат:
['Laptop']
['Mouse']
['Keyboard']
При каждом вызове без второго аргумента выполняется:
products = []
поэтому создаётся новый список.
6. Почему именно None
None — неизменяемый singleton-объект, который удобно использовать как специальное значение:
Аргумент не был передан — нужно создать объект внутри функции.
def add_product(
product,
products=None,
):
if products is None:
products = []
Проверка выполняется через:
is None
Переданный список продолжает работать нормально
7. Пользователь функции может передать собственный список
def add_product(
product,
products=None,
):
if products is None:
products = []
products.append(product)
return products
Создадим список:
cart = [
'Laptop',
]
Передадим его:
add_product(
'Mouse',
cart,
)
print(cart)
Результат:
['Laptop', 'Mouse']
Здесь это ожидаемое поведение.
Мы сами передали изменяемый объект функции, поэтому функция работает именно с ним.
Проблема возникает не в том, что функции нельзя принимать списки, а в использовании одного изменяемого объекта как значения параметра по умолчанию.
Плохой и хороший вариант
8. Не рекомендуется
def create_order(
product,
products=[],
):
products.append(product)
return products
9. Рекомендуемый вариант
def create_order(
product,
products=None,
):
if products is None:
products = []
products.append(product)
return products
Официальная документация Python отдельно предупреждает:
значение параметра по умолчанию вычисляется только один раз.
Поэтому изменяемые объекты вроде списков и словарей обычно не следует использовать как значения по умолчанию.
Python FAQ рекомендует использовать:
None
а нужный изменяемый объект создавать уже внутри функции.
Главное по теме
Значения параметров по умолчанию создаются не при каждом вызове функции, а при выполнении её определения.
Поэтому:
def function(values=[]):
...
создаёт один список, который будет повторно использоваться следующими вызовами функции без этого аргумента.
Если функция изменяет этот список:
values.append(...)
изменения сохранятся и будут видны при следующих вызовах.
Поэтому вместо:
def function(values=[]):
...
обычно пишем:
def function(values=None):
if values is None:
values = []
Так новый список создаётся отдельно для каждого вызова, которому собственный список не был передан.
Официальное объяснение есть в Python Tutorial и Programming FAQ. Python Tutorial — Default Argument Values Python FAQ — Why are default values shared between objects?