Как стать автором
Обновить
393.88
Рейтинг
ДомКлик
Место силы

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

Блог компании ДомКлик Python *Программирование *
🔥 Технотекст 2020
В языках программирования меня всегда интересовало их внутреннее устройство. Как работает тот или иной оператор? Почему лучше писать так, а не иначе? Подобные вопросы не всегда помогают решить задачу «здесь и сейчас», но в долгосрочной перспективе формируют общую картину языка программирования. Сегодня я хочу поделиться результатом одного из таких погружений и ответить на вопрос, что происходит при модификации 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])

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

UPD. Коллеги подсказали, что этот пример так же разобран в книге Fluent Python Лучано Ромальо. Очень рекомендуют ее почитать всем заинтересованным
Теги: pythonразработкаотладка
Хабы: Блог компании ДомКлик Python Программирование
Всего голосов 106: ↑104 и ↓2 +102
Комментарии 51
Комментарии Комментарии 51

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
domclick.ru
Численность
501–1 000 человек
Дата регистрации
Представитель
Мария Ланшакова

Блог на Хабре