Попробуйте решить эти три задачи, а потом сверьтесь с ответами в конце статьи.
Совет: у задач есть кое-что общее, поэтому освежите в памяти решение первой задачи, когда перейдёте ко второй или третьей, так вам будет проще.
Первая задача
Есть несколько переменных:
x = 1
y = 2
l = [x, y]
x += 5
a = [1]
b = [2]
s = [a, b]
a.append(5)
Что будет выведено на экран при печати
l
и s
?Вторая задача
Определим простую функцию:
def f(x, s=set()):
s.add(x)
print(s)
Что будет, если вызвать:
>>f(7)
>>f(6, {4, 5})
>>f(2)
Третья задача
Определим две простые функции:
def f():
l = [1]
def inner(x):
l.append(x)
return l
return inner
def g():
y = 1
def inner(x):
y += x
return y
return inner
Что мы получим после выполнения этих команд?
>>f_inner = f()
>>print(f_inner(2))
>>g_inner = g()
>>print(g_inner(2))
Насколько вы уверены в своих ответах? Давайте проверим вашу правоту.
Решение первой задачи
>>print(l)
[1, 2]
>>print(s)
[[1, 5], [2]]
Почему второй список реагирует на изменение своего первого элемента
a.append(5)
, а первый список полностью игнорирует такое же изменение x+=5
?Решение второй задачи
Посмотрим, что произойдёт:
>>f(7)
{7}
>>f(6, {4, 5})
{4, 5, 6}
>>f(2)
{2, 7}
Погодите, разве последним результатом не должно быть
{2}
?Решение третьей задачи
Результат будет таким:
>>f_inner = f()
>>print(f_inner(2))
[1, 2]
>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable ‘y’ referenced before assignment
Почему
g_inner(2)
не выдала 3
? Почему внутренняя функция f()
помнит о внешней области видимости, а внутренняя функция g()
не помнит? Они же практически идентичны!Объяснение
Что если я скажу вам, что все эти примеры странного поведения связаны с различием между изменяемыми и неизменяемыми объектами в Python?
Изменяемые объекты, такие как списки, множества или словари, могут быть изменены на месте. Неизменяемые объекты, такие как числовые и строковые значения, кортежи, не могут быть изменены; их «изменение» приведёт к созданию новых объектов.
Объяснение первой задачи
x = 1
y = 2
l = [x, y]
x += 5
a = [1]
b = [2]
s = [a, b]
a.append(5)
>>print(l)
[1, 2]
>>print(s)
[[1, 5], [2]]
Поскольку
x
неизменяема, операция x+=5
не меняет исходный объект, а создаёт новый. Но первый элемент списка всё ещё ссылается на исходный объект, поэтому его значение не меняется.Т.к. a изменяемый объект, то команда
a.append(5)
меняет исходный объект (а не создает новый), и список s
«видит» изменения.Объяснение второй задачи
def f(x, s=set()):
s.add(x)
print(s)
>>f(7)
{7}
>>f(6, {4, 5})
{4, 5, 6}
>>f(2)
{2, 7}
С первыми двумя результатами всё понятно: первое значение
7
добавляется к изначально пустому множеству и получается {7}
; потом значение 6
добавляется к множеству {4, 5}
и получается {4, 5, 6}
.А потом начинаются странности. Значение
2
добавляется не к пустому множеству, а к {7}. Почему? Исходное значение опционального параметра s
вычисляется только один раз: при первом вызове s будет инициализировано как пустое множество. А поскольку оно изменяемое, после вызова f(7)
оно будет будет изменено “на месте”. Второй вызов f(6, {4, 5})
не повлияет на параметр по умолчанию: его заменяет множество {4, 5}
, то есть {4, 5}
является другой переменной. Третий вызов f(2)
использует ту же переменную s
, что использовалась при первом вызове, но она не переинициализируется как пустое множество, а вместо этого берётся её предыдущее значение {7}
.Поэтому не следует использовать изменяемые аргументы в качестве аргументов по умолчанию. В этом случае функцию нужно изменить:
def f(x, s=None):
if s is None:
s = set()
s.add(x)
print(s)
Объяснение третьей задачи
def f():
l = [1]
def inner(x):
l.append(x)
return l
return inner
def g():
y = 1
def inner(x):
y += x
return y
return inner
>>f_inner = f()
>>print(f_inner(2))
[1, 2]
>>g_inner = g()
>>print(g_inner(2))
UnboundLocalError: local variable ‘y’ referenced before assignment
Здесь мы имеем дело с замыканиями: внутренние функции помнят, как выглядели их внешние пространства имён на момент своего определения. Или хотя бы должны помнить, однако вторая функция делает покерфейс и ведёт себя так, словно не слышала о своём внешнем пространстве имён.
Почему так происходит? Когда мы исполняем
l.append(x)
, меняется изменяемый объект, созданный при определении функции. Но переменная l
всё ещё ссылается на старый адрес в памяти. Однако попытка изменить неизменяемую переменную во второй функции y += x
приводит к тому, что y начинает ссылаться на другой адрес в памяти: исходная y будет забыта, что приведёт к ошибке UnboundLocalError.Заключение
Разница между изменяемыми и неизменяемыми объектами в Python очень важна. Избегайте странного поведения, описанного в этой статье. В особенности:
- Не используйте по умолчанию изменяемые аргументы.
- Не пытайтесь менять неизменяемые переменные-замыкания во внутренних функциях.
gmixo
со значением по умолчанию в функции вообще внимательным нужно быть,
скорее всего результат будет не тот которого ожидаешь
Deerenaros
Поэтому правильный вариант, очевидно:
Хотя можно и чуточку усложнить, дабы предупредить более "плоское" использование, но тогда логике посыпится в редких случаях, но они специфичны и довольно легко обходятся:
Вообще, статья какая-то игрушечная, я бы сказал — кликбейторская. Обладая простейшим пониманием как работают ссылки (и немного про область видимости в третьей "задаче") — "решения" наиочевиднейшие.
gmixo
вообще, если на вход ожидается datetime,
мне больше нравится вот такое решение
Deerenaros
Это если ожидается datetime, здесь это никоим образом не продемонстрированно, а из контекста непонятно. Но и такой вариант жизнеспособен, конечно.
khim
Только такой вариант я и видел в качестве «рекомендованного».
А вот этот вот setter — это на цирковой трюк похоже. Потому что запаковывать дату с лямбду можно, конечно — но очень уж напоминает вырезание гланд через задний проход.
Deerenaros
Тут вопрос в использовании данных. Если функция быстрая — то всё хорошо, но если медленная/асинхронная, а то и вовсе — переодическая — то у нас проблемы. Подобные подходы в ORM используются, например.