Хабрахабр

[Перевод] А вы можете решить эти три (обманчиво) простые задачи на Python?

С самого начала своего пути как разработчика программного обеспечения я очень любил копаться во внутренностях языков программирования. Мне всегда было интересно, как устроена та или иная конструкция, как работает та или иная команда, что под капотом у синтаксического сахара и т.п. Недавно мне на глаза попалась интересная статья с примерами того, как не всегда очевидно работают mutable- и immutable-объекты в Python. На мой взгляд, ключевое — это то, как меняется поведение кода в зависимости от используемого типа данных, при сохранении идентичной семантики и используемых языковых конструкциях. Это отличный пример того, что думать надо не только при написании, но и при использовании. Предлагаю всем желающим ознакомиться с переводом.

Попробуйте решить эти три задачи, а потом сверьтесь с ответами в конце статьи.

Совет: у задач есть кое-что общее, поэтому освежите в памяти решение первой задачи, когда перейдёте ко второй или третьей, так вам будет проще.

Первая задача

Есть несколько переменных:

x = 1y = 2l = [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 = 1y = 2l = [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 очень важна. Избегайте странного поведения, описанного в этой статье. В особенности:

  • Не используйте по умолчанию изменяемые аргументы.
  • Не пытайтесь менять неизменяемые переменные-замыкания во внутренних функциях.
Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть