Правильное решение вас удивит
Попробуйте решить эти задачи самостоятельно, а потом проверьте себя по готовым ответам.
Подсказка: У всех задач есть нечто общее. Так что если разобраться в решении первой задачи, то решать остальные будет гораздо проще.
Задача 1
Представьте, что у вас есть несколько переменных:
x = 1
y = 2
l = [x, y]
x += 5
a = [1]
b = [2]
s = [a, b]
a.append(5)
Какой результат вам даст вывод l
и s
?
Задача 2
Давайте определим простую функцию:
def f(x, s=set()):
s.add(x)
print(s)
Что произойдет при вызове:
>>f(7)
>>f(6, {4, 5})
>>f(2)
?
Задача 3
Давайте определим две простые функции:
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))
Насколько вы уверены в своих ответах? Давайте узнаем правильное решение.
Решение задачи 1
>>print(l)
[1, 2]
>>print(s)
[[1, 5], [2]]
Почему второй список реагирует на изменение своего первого элемента a.append(5)
, а первый список полностью игнорирует похожее изменение x+=5
?
Решение задачи 2
Давайте узнаем, что произойдет:
>>f(7)
{7}
>>f(6, {4, 5})
{4, 5, 6}
>>f(2)
{2, 7}
Стоп, а разве последний вывод не должен быть {2}
?
Решение задачи 3
Результаты будут следующими:
>>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?
Изменяемые объекты (списки, наборы или словари) могут изменяться (мутировать) на месте. Неизменяемые объекты (целые числа, строки и кортежи) не могут изменяться. «Изменение» таких объектов приводит к созданию нового объекта.
Объяснение задачи 1
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
«видит» эти изменения.
Объяснение задачи 2
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
запускается как пустое множество. А поскольку s
является изменяемым объектом, то после вызова f(7)
он изменяется на месте. Второй вызов f(6, {4, 5})
не влияет на исходный параметр, поскольку представленное множество {4, 5}
скрывает его. Иначе говоря, {4, 5}
является другой переменной. Третий вызов f(2)
использует ту же переменную s
, которая была в первом вызове. Но в этот раз s
вызывается не как пустое множество, а со своим предыдущим значением {7}
.
Вот почему вам не следует использовать изменяемые аргументы по умолчанию. В таком случае функция будет изменяться следующим образом:
def f(x, s=None):
if s is None:
s = set()
s.add(x)
print(s)
Объяснение задачи 3
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)
, изменяемый объект, созданный в момент определения, изменяется, но переменная продолжает указывать на тот же адрес в памяти. Однако попытка изменить неизменяемую переменную во второй функции y += x
приводит к тому, что y
начинает указывать на другой адрес в памяти. Исходная y
больше не запоминается, и возникает ошибка UnboundLocalError
.
Заключение
В Python очень важно разграничивать изменяемые и неизменяемые объекты. Во избежание странного поведения кода (как в примерах выше) нужно помнить о главном:
- не используйте изменяемые аргументы по умолчанию;
- не пытайтесь изменять неизменяемые замкнутые переменные во внутренних функциях.
Читайте также:
- Как создавать и публиковать консольные приложения на Python
- Проверка типов в Python
- Attr - одна из лучших практик объектно-ориентированного Python
Перевод статьи Maria Fabiańska: Can you solve these 3 (seemingly) easy Python problems?