Правильное решение вас удивит

Попробуйте решить эти задачи самостоятельно, а потом проверьте себя по готовым ответам.

Подсказка: У всех задач есть нечто общее. Так что если разобраться в решении первой задачи, то решать остальные будет гораздо проще.

Задача 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 очень важно разграничивать изменяемые и неизменяемые объекты. Во избежание странного поведения кода (как в примерах выше) нужно помнить о главном:

  • не используйте изменяемые аргументы по умолчанию;
  • не пытайтесь изменять неизменяемые замкнутые переменные во внутренних функциях.

Читайте также:


Перевод статьи Maria Fabiańska: Can you solve these 3 (seemingly) easy Python problems?

Предыдущая статьяУглубление в параметры ядра. Часть 1: загрузочные параметры
Следующая статьяПять алиасов Git, без которых мне не прожить