Как писать понятный код


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

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

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

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

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

Перевод статьи


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.