В языках программирования меня всегда интересовало их внутреннее устройство. Как работает тот или иной оператор? Почему лучше писать так, а не иначе? Подобные вопросы не всегда помогают решить задачу «здесь и сейчас», но в долгосрочной перспективе формируют общую картину языка программирования. Сегодня я хочу поделиться результатом одного из таких погружений и ответить на вопрос, что происходит при модификации
Все мы знаем, что в 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 Лучано Ромальо. Очень рекомендуют ее почитать всем заинтересованным