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 Лучано Ромальо. Очень рекомендуют ее почитать всем заинтересованным
justhabrauser
Странный чувак…
"В tuple нельзя заменять элементы". Просто запомни.
Но если элемент изменяемый, то внутри него можно ковыряться.
Пример:
grmood
ну так о том и речь, что это, фактически, — тот же список внутри тупла, его замена не происходит (если логика с id(a) в начале статьи — верная), элемент тупла действительно меняется без проблем, как вы и говорите, но в добавок мы имеем еще и исключение, не влияющее на результат.
justhabrauser
Логика в начале статьи не до конца верная.
Если операция += заменяет содержимое старого объекта полностью новым, то автор ничего не заметит в промежутке.
А питон — заметит.
А результатом мы получили exception. То, что list таки расширился, это результат UB.
PS. А вообще такие статьи похожи на изучение взаимодействия бензопилы и арматуры. С получением удивительных (для автора) результатов.
fantomius Автор
Ну хз. Лично мне кажется не совсем интуитивным, когда ты с одной стороны ловишь исключение, а с другой стороны действие выполняется. Это кажется немного странным. И ровно эта причина заставила поковыряться в байт-коде и разобраться как оно внутри устроено.
justhabrauser
Вы можете на C сделать нечто вроде "void *ptr = &(main); *ptr = 0x00;" и тоже получить результат. И затертый main и exception.
Но это повод ковыряться не в коде, а в документации. Где написано "не надо так делать".
grmood
P. S. вместо
должно быть, вероятно, что-то вроде , т. к. вы разыменовываете void и void'у целое присваиваете, оно просто не откомпилируется (invalid use of void expression)Я вот кстати не знаю, является ли этот пример именно Undefined Behavior или тут Unspecified, не являюсь знатоком стандарта.
Но вообще соглашусь с вами по части того, что документацию стоит читать в таких ситуациях, ее разбора не хватает в статье для полноты картины :)
justhabrauser
Возможно это будет откровением, но seg fault — это и есть исключение.
Выбрасываемое ядром.
Написанном, кстати, на чистокровном С.
Но таки да — если "маю час, маю натхнення", то в коде поковыряться всегда полезно)
mayorovp
Это какая-то нестандартная терминология...
grmood
Давайте я вам тоже накидаю пару «откровений».
Выше вы писали: что и main перетрется, и exception будет:
Так вот:
1. main — не перетрется, потому что сегмент кода защищен от записи, от того то вы и получаете segfault от ОС.
2. Да, конечно, segfault — это что-то ВРОДЕ исключения в некотором смысле, только оно «выбрасывается» ядром как сигнал процессу (в линухе, так-то поведение может быть разное, зависит от ОС). А обрабатывается ядром в данном случае вполне себе аппаратная ошибка доступа в память на запись, помеченную недоступной для таковой в таблице страниц (аппаратная, потому что таблица страниц и все вот это вот имеет, как правило, аппаратную реализацию). Разницу с языковыми стандартами не улавливаете?
Может быть, кстати, еще для вас станет откровением, что ядро написано не только на Си, но имеет еще и платформозависимые части с соотв. ассемблерами, а так же то, что Си там далеко не чистокровный.
Ну и возвращаясь к нашим баранам, main, как я сказал, не перетрется, так что ваш пример — полная противоположность поведению интерпретатора из данной статьи, ибо сайд эффекта как раз в вашем примере — нет.
Vindicar
Вы путаете исключения C++ (с C не работал), которые есть языковые конструкции и потому работают строго синхронно с операторами языка (грубо говоря, каждое исключение есть результат выполнения оператора throw где-то) и исключения платформы x86 (защищенный и длинный режим), которые есть частный случай прерываний. Существуют механизмы, позволяющие исключения x86 (те из них, которые ОС пробрасывает в процесс) заворачивать в исключения C++, но они, насколько я знаю, не являются стандартными, и потому разные компиляторы их реализуют по-разному.
mi1kyway
Поковыряться в байт-коде — это завсегда интересное занятие, но можно было обойтись простым рассуждением: выражение a[2] += [4, 5] из Вашего примера должно быть эквивалентно по результату выражению a[2] = a[2] + [4,5], в котором, согласно приоритету операций, сначала выполнится правая часть, модифицирующая список, а затем настанет очередь оператора присваивания, который не поддерживается для элементов кортежа. Так что поведение интерпретатора выглядит вполне логичным.
fantomius Автор
На самом деле с точки зрения логики — да, но с точки зрения реализации += и + могут быть реализованы по разному
Кроме того, семантически
a[2] += [3,4]
эквивалентно
b = a[2]
b += [3,4]
а второй код уже отработает без исключения =)
mi1kyway
Но если с точки зрения логики всё работает правильно, тогда описанное поведение списка в кортеже выглядит вполне стандартно для языка, хоть и не совсем интуитивно.
А куда в эквивалентной записи подевалось a[2] = b?
EzikBro
В питоне присваивание массивов идет по ссылке, а не копированием, так что обратное присваивание не требуется.
arthuriantech
Без финального присваивания эквивалент не будет работать для иммутабельных типов вроде int, str, tuple, frozenset etc где вместо модификации создается копия.
longclaps
a[2] += [3, 4]
точно эквивалентноa[2].extends([3, 4])
, что означает, чтоid(a[2])
не меняется, и стало быть контракт для элемента кортежа не нарушается.Pavlik007
По-моему, в вашем рассуждении ошибка. Вы пишете, что a[2] += [4, 5] должно быть эквивалентно по результату выражению a[2] = a[2] + [4,5], в котором сперва выполнится правая часть, модифицирующая список. Но правая часть не модифицирует список. Она создаёт новый список.
Вот исходный пример (использующий список вместо кортежа, чтобы избежать исключений):
>>> l1 = [[1,2]]
>>> l = l1[0] # сохраним ссылку, чтобы избежать удаления списка
>>> hex(id(l1[0]))
'0x6fffffebfe88'
>>> l1[0] += [3, 4]
>>> hex(id(l1[0]))
'0x6fffffebfe88' # id тот же, что и выше
Вот пример с конструкцией из вашего рассуждения:
>>> l2 = [[1,2]]
>>> l = l2[0] # сохраним ссылку, чтобы избежать удаления списка
>>> hex(id(l2[0]))
'0x6fffffe428c8'
>>> l2[0] = l2[0] + [3, 4]
>>> hex(id(l2[0]))
'0x6fffffe427c8' # id изменился, так как был создан новый список
mi1kyway
Действительно, моя попытка рассмотреть ситуацию «на пальцах» не совсем удачная, в документации всё изложено значительно лучше.
s_suhanov
Но ведь это совсем не так. Автор во втором абзаце это же и объясняет.
tyomitch
Каким это образом правая часть, т.е.
a[2] + [4,5]
, модифицирует список?MooNDeaR
Добро пожаловать в мир UB) C++ программисты с этим каждый день живут)
tyomitch
В Python не может быть UB, потому что нет формального стандарта, а значит, весь язык — одно большое UB.
mayorovp
А куда этот стандарт, со всеми PEP, делся?
tyomitch
Где вы его видели?