Почему список в кортеже ведет себя странно в 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 "", line 1, in
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]
Варианты:
- Добавятся элементы в список.
- Вылетит исключение о неизменяемости tuple.
- И то, и другое.
- Ни то, ни другое.
Запишите свой ответ на бумажке и давайте сделаем небольшую проверку:
>>> a = (1,2,[3,4])
>>> a[2] += [4,5]
Traceback (most recent call last):
File "", line 1, in
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])
Спасибо всем, кто дочитал до конца. Надеюсь, было интересно =)