Почему список в кортеже ведет себя странно в Python?

от автора

В языках программирования меня всегда интересовало их внутреннее устройство. Как работает тот или иной оператор? Почему лучше писать так, а не иначе? Подобные вопросы не всегда помогают решить задачу «здесь и сейчас», но в долгосрочной перспективе формируют общую картину языка программирования. Сегодня я хочу поделиться результатом одного из таких погружений и ответить на вопрос, что происходит при модификации tuple‘а в list‘е.

Все мы знаем, что в Python есть тип данных list:

a = [] a.append(2)

list — это просто массив. Он позволяет добавлять, удалять и изменять элементы. Также он поддерживает много разных интересных операторов. Например, оператор += для добавления элементов в list. += меняет текущий список, а не создает новый. Это хорошо видно тут:

>>> a = [1,2] >>> id(a) 4543025032 >>> a += [3,4] >>> id(a) 4543025032

В Python есть еще один замечательный тип данных: tuple — неизменяемая коллекция. Она не позволяет добавлять, удалять или менять элементы:

>>> a = (1,2) >>> a[1] = 3 Traceback (most recent call last):   File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment

При использовании оператора += создается новый tuple:

>>> a = (1,2) >>> id(a) 4536192840 >>> a += (3,4) >>> id(a) 4542883144

Внимание, вопрос: что сделает следующий код?

a = (1,2,[3,4]) a[2] += [4,5]

Варианты:

  1. Добавятся элементы в список.
  2. Вылетит исключение о неизменяемости tuple.
  3. И то, и другое.
  4. Ни то, ни другое.

Запишите свой ответ на бумажке и давайте сделаем небольшую проверку:

>>> a = (1,2,[3,4]) >>> a[2] += [4,5] Traceback (most recent call last):   File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment

Ну что же! Вот мы и разобрались! Правильный ответ — 2. Хотя, подождите минутку:

>>> a (1, 2, [3, 4, 4, 5])

На самом деле правильный ответ — 3. То есть и элементы добавились, и исключение вылетело — wat?!

Давайте разберемся, почему так происходит. И поможет нам в этом замечательный модуль dis:

import dis  def foo():     a = (1,2,[3,4])     a[2] += [4,5]  dis.dis(foo)   2     0 LOAD_CONST      1 (1)         3 LOAD_CONST      2 (2)         6 LOAD_CONST      3 (3)         9 LOAD_CONST      4 (4)        12 BUILD_LIST      2        15 BUILD_TUPLE     3        18 STORE_FAST      0 (a)    3    21 LOAD_FAST       0 (a)        24 LOAD_CONST      2 (2)        27 DUP_TOP_TWO        28 BINARY_SUBSCR        29 LOAD_CONST      4 (4)        32 LOAD_CONST      5 (5)        35 BUILD_LIST      2        38 INPLACE_ADD        39 ROT_THREE        40 STORE_SUBSCR        41 LOAD_CONST      0 (None)        44 RETURN_VALUE

Первый блок отвечает за построение tuple‘а и его сохранение в переменной a. Дальше начинается самое интересное:

       21 LOAD_FAST       0 (a)        24 LOAD_CONST      2 (2)

Загружаем в стек указатель на переменную a и константу 2.

       27 DUP_TOP_TWO

Дублируем их и кладем в стек в том же порядке.

       28 BINARY_SUBSCR

Этот оператор берет верхний элемент стека (TOS) и следующий за ним (TOS1). И записывает на вершину стека новый элемент TOS = TOS1[TOS]. Так мы убираем из стека два верхних значения и кладем в него ссылку на второй элемент tuple‘а (наш массив).

       29 LOAD_CONST      4 (4)        32 LOAD_CONST      5 (5)        35 BUILD_LIST      2

Строим список из элементов 4 и 5 и кладем его на вершину стека:

       38 INPLACE_ADD

Применяем += к двум верхним элементам стека (Важно! Это два списка! Один состоит из 4 и 5, а другой взяты из tuple). Тут всё нормально, инструкция выполняется без ошибок. Поскольку += изменяет оригинальный список, то список в tuple‘е уже поменялся (именно в этот момент).

       39 ROT_THREE        40 STORE_SUBSCR

Тут мы меняем местами три верхних элемента стека (там живет tuple, в нём индекс массива и новый массив) и записываем новый массив в tuple по индексу. Тут-то и происходит исключение!

Ну что же, вот и разобрались! На самом деле список менять можно, а падает всё на операторе =.

Давайте напоследок разберемся, как переписать этот код без исключений. Как мы уже поняли, надо просто убрать запись в tuple. Вот парочка вариантов:

>>> a = (1,2,[3,4]) >>> b = a[2] >>> b += [4,5] >>> a (1, 2, [3, 4, 4, 5])

>>> a = (1,2,[3,4]) >>> a[2].extend([4,5]) >>> a (1, 2, [3, 4, 4, 5])

Спасибо всем, кто дочитал до конца. Надеюсь, было интересно =)

ссылка на оригинал статьи https://habr.com/ru/company/domclick/blog/506138/


Комментарии

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

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