В языках программирования меня всегда интересовало их внутреннее устройство. Как работает тот или иной оператор? Почему лучше писать так, а не иначе? Подобные вопросы не всегда помогают решить задачу «здесь и сейчас», но в долгосрочной перспективе формируют общую картину языка программирования. Сегодня я хочу поделиться результатом одного из таких погружений и ответить на вопрос, что происходит при модификации
Все мы знаем, что в Python есть тип данных
В Python есть еще один замечательный тип данных:
При использовании оператора
Внимание, вопрос: что сделает следующий код?
Варианты:
Запишите свой ответ на бумажке и давайте сделаем небольшую проверку:
Ну что же! Вот мы и разобрались! Правильный ответ — 2. Хотя, подождите минутку:
На самом деле правильный ответ — 3. То есть и элементы добавились, и исключение вылетело — wat?!

Давайте разберемся, почему так происходит. И поможет нам в этом замечательный модуль
Первый блок отвечает за построение
Загружаем в стек указатель на переменную
Дублируем их и кладем в стек в том же порядке.
Этот оператор берет верхний элемент стека (TOS) и следующий за ним (TOS1). И записывает на вершину стека новый элемент
Строим список из элементов 4 и 5 и кладем его на вершину стека:
Применяем
Тут мы меняем местами три верхних элемента стека (там живет
Ну что же, вот и разобрались! На самом деле список менять можно, а падает всё на операторе
Давайте напоследок разберемся, как переписать этот код без исключений. Как мы уже поняли, надо просто убрать запись в
Спасибо всем, кто дочитал до конца. Надеюсь, было интересно =)
UPD. Коллеги подсказали, что этот пример так же разобран в книге Fluent 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]Варианты:
- Добавятся элементы в список.
- Вылетит исключение о неизменяемости tuple.
- И то, и другое.
- Ни то, ни другое.
Запишите свой ответ на бумажке и давайте сделаем небольшую проверку:
>>> 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])Спасибо всем, кто дочитал до конца. Надеюсь, было интересно =)
UPD. Коллеги подсказали, что этот пример так же разобран в книге Fluent Python Лучано Ромальо. Очень рекомендуют ее почитать всем заинтересованным