Комментарии 51
Странный чувак…
"В tuple нельзя заменять элементы". Просто запомни.
Но если элемент изменяемый, то внутри него можно ковыряться.
Пример:
>>> t = (1, 2, [3, 4])
>>> t[2][1] = 5
>>> t
(1, 2, [3, 5])
ну так о том и речь, что это, фактически, — тот же список внутри тупла, его замена не происходит (если логика с id(a) в начале статьи — верная), элемент тупла действительно меняется без проблем, как вы и говорите, но в добавок мы имеем еще и исключение, не влияющее на результат.
Логика в начале статьи не до конца верная.
Если операция += заменяет содержимое старого объекта полностью новым, то автор ничего не заметит в промежутке.
А питон — заметит.
А результатом мы получили exception. То, что list таки расширился, это результат UB.
PS. А вообще такие статьи похожи на изучение взаимодействия бензопилы и арматуры. С получением удивительных (для автора) результатов.
Вы можете на C сделать нечто вроде "void *ptr = &(main); *ptr = 0x00;" и тоже получить результат. И затертый main и exception.
Но это повод ковыряться не в коде, а в документации. Где написано "не надо так делать".
- в Си нет исключений
- Что в Си, что в Си++ вы скорее всего получите ошибку сегментации во время исполнения, в Си++ нет возможности ее обработать как исключение (с т зрения языка во всяком случае)
- Повод ковыряться в документации действительно есть, но в коде поковыряться не менее полезно :)
P. S. вместо
void <em>ptr = &(main);
должно быть, вероятно, что-то вроде int </em>ptr = (int <em>)&(main);
, т. к. вы разыменовываете void и void'у целое присваиваете, оно просто не откомпилируется (invalid use of void expression)Я вот кстати не знаю, является ли этот пример именно Undefined Behavior или тут Unspecified, не являюсь знатоком стандарта.
Но вообще соглашусь с вами по части того, что документацию стоит читать в таких ситуациях, ее разбора не хватает в статье для полноты картины :)
Возможно это будет откровением, но seg fault — это и есть исключение.
Выбрасываемое ядром.
Написанном, кстати, на чистокровном С.
Но таки да — если "маю час, маю натхнення", то в коде поковыряться всегда полезно)
Это какая-то нестандартная терминология...
Возможно это будет откровением, но
Давайте я вам тоже накидаю пару «откровений».
Выше вы писали: что и main перетрется, и exception будет:
и тоже получить результат. И затертый main и exception
Так вот:
1. main — не перетрется, потому что сегмент кода защищен от записи, от того то вы и получаете segfault от ОС.
2. Да, конечно, segfault — это что-то ВРОДЕ исключения в некотором смысле, только оно «выбрасывается» ядром как сигнал процессу (в линухе, так-то поведение может быть разное, зависит от ОС). А обрабатывается ядром в данном случае вполне себе аппаратная ошибка доступа в память на запись, помеченную недоступной для таковой в таблице страниц (аппаратная, потому что таблица страниц и все вот это вот имеет, как правило, аппаратную реализацию). Разницу с языковыми стандартами не улавливаете?
Написанном, кстати, на чистокровном С.
Может быть, кстати, еще для вас станет откровением, что ядро написано не только на Си, но имеет еще и платформозависимые части с соотв. ассемблерами, а так же то, что Си там далеко не чистокровный.
Ну и возвращаясь к нашим баранам, main, как я сказал, не перетрется, так что ваш пример — полная противоположность поведению интерпретатора из данной статьи, ибо сайд эффекта как раз в вашем примере — нет.
Кроме того, семантически
a[2] += [3,4]
эквивалентно
b = a[2]
b += [3,4]
а второй код уже отработает без исключения =)
А куда в эквивалентной записи подевалось a[2] = b?
a[2] += [3, 4]
точно эквивалентно a[2].extends([3, 4])
, что означает, что id(a[2])
не меняется, и стало быть контракт для элемента кортежа не нарушается.Вот исходный пример (использующий список вместо кортежа, чтобы избежать исключений):
>>> 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 изменился, так как был создан новый список
Ниже привели ссылку на документацию, которая описывает происходящее более детально.
выражение a[2] += [4, 5] из Вашего примера должно быть эквивалентно по результату выражению a[2] = a[2] + [4,5]
Но ведь это совсем не так. Автор во втором абзаце это же и объясняет.
выражению a[2] = a[2] + [4,5], в котором, согласно приоритету операций, сначала выполнится правая часть, модифицирующая список, а затем настанет очередь оператора присваивания
Каким это образом правая часть, т.е.
a[2] + [4,5]
, модифицирует список?Добро пожаловать в мир UB) C++ программисты с этим каждый день живут)
Потому что когда вылетает исключение — ты ожидаешь, что операция не выполнилась, а она по факту выполнилась.
Поэтому и хотелось заглянуть внутрь.
Потому что когда вылетает исключение — ты ожидаешь, что операция не выполнилась, а она по факту выполнилась.а как же выход за границы массива, допустим 999 вставок из 1000 выполнилось, а на 1000 возникло исключение?
Я только учу питон и возможно мой вопрос покажется глупым, но почему в предпоследнем фрагменте кода при изменении "b" поменялась и "а"?
Тот факт, что при возникновении исключения ещё и сайд-эффект выполнился, похоже на баг. Может, об этом стоит создать issue?
Можно подавить исключение, разрешив присваивание элемента tuple себе, т.е. b = a[2]; a[2] = b сработает. Но это еще более странное поведение.
Я подозреваю, дело в том, что по умолчанию a[i] += b разворачивается в
temp = a[i]
temp = temp.__iadd__(b)
a[i] = temp #exception!
Но вообще это поведение уже описано в FAQ Python.
По-идее, это дыра в логике. У объекта могут быть методы __add__
и __iadd__
, и если первый всегда создаёт новый объект (а потому должен вернуть значение), то второй нового объекта не создаёт и значения возвращать не должен бы.
То есть x += y
должно выполняться один из двух способов (в зависимости от существования метода __iadd__
):
x = x.__add__(y)
x.__iadd__(y)
И, если бы так оно и было, то и проблемы со списком в кортеже не было бы.
Но в реальности второй вариант выполняется как x = x.__iadd__(y)
, то есть методу __iadd__
зачем-то разрешено одновременно и модифицировать старый объект, и вернуть новый — и отсюда и растёт обсуждаемая проблема.
Но исправить эту дыру уже не получится, ведь это потеря обратной совместимости, а экосистема Питона до сих пор от перехода на третью версию не восстановилась.
Это сделано для того, чтобы работа с примитивными типами не вызывала внезапных wtf.
x = 5
y = x
x += 3
Вы же не ожидаете, что в этом случае у изменится?
А с какого перепугу в такой ситуации y должен измениться?
Примитивы иммутабелтны, у иммутабельных типов оператора __iadd__
не должно быть, значит оператор +=
должен сработать как y = y.__add__(x)
.
В питоне же динамическая типизация, если код += находится в функции, то компилятор не сможет заменить iadd на add.
Ау, но он же уже так делает! Вот, только что проверил:
Python 3.8.2 (default, Feb 26 2020, 02:56:10)
> type(0)
<class 'int'>
> type(0).__add__
<slot wrapper '__add__' of 'int' objects>
> type(0).__iadd__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'int' has no attribute '__iadd__'
Как видно, у чисел метода __iadd__
не существует, но это никак не мешает оператору +=
работать.
Выражение
a[2] += [4, 5]
является буквальным эквивалентом этого:
i = a[2]
i += [4, 5]
a[2] = i # exception
Первый пункт, «ты ожидаешь, что операция не выполнилась, а она по факту выполнилась.»
Пример кода:
>>> A = []
>>> def add_to_A():
... A.append(len(A))
>>> 1 / add_to_A()
Traceback (most recent call last):
File "<input>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'int' and 'NoneType'
>>> A
[0]
Если в строке два действия, и интерпретация падает на втором, то первое-то выполняется.Второй пункт, какого чёрта это вообще падает?
В кортеже нельзя менять ссылки на объекты. А операция += вызывает __iadd__ у объекта, который может и новую ссылку вернуть. Поэтому операция типа
a[2] += [4,5]
может изменить ссылку в кортеже.class Foo:
def __iadd__(self, other):
return Foo()
a = b = Foo()
print(id(a)) # 1424399578256
a += 1
print(id(a)) # 1424400155360
print(id(b), id(a), a is b) # 2088577971344 2088579662560 False
Третье. «семантически a[2] += [3,4] эквивалентно b = a[2]; b += [3,4]»
В свете примера выше ни разу не эквивалентно.
class Foo:
def __iadd__(self, other):
return Foo()
A = [Foo()]
print(A) # [<__main__.Foo object at 0x0000019D4A71B490>]
b = A[0]
b += 1 # Всё, теперь в b ссылка вообще на другой объект
print(A) # [<__main__.Foo object at 0x0000019D4A71B490>]
A[0] += 1 # Вот только теперь ссылку испортили
print(A) # [<__main__.Foo object at 0x0000019D4A9D6820>]
Теперь сравнение с другими языками. Первый пример нет смысла обсуждать: почти везде так.
Второй. Ну, например, js. Javascript тоже красавчег:
> A = Object.freeze([1, 2, 3])
< (3) [1, 2, 3]
> A[1] += 10
< 12
> A
< (3) [1, 2, 3]
Круто. Исключения нет, вывод в консоль правильный. Но действия тоже нет :)
Третье.
> A = [1, [2], 3]
< (3) [1, Array(1), 3]
> A
< (3) [1, Array(1), 3]
> b = A[1]
< [2]
> b += 1
< "21"
> A
< (3) [1, Array(1), 3]
> A[1] += 1
< "21"
> A
< (3) [1, "21", 3]
Ну логично же. Мы взяли другую переменную и поменяли. Исходный объект-то тут причём?docs.python.org/3.8/faq/programming.html?highlight=comprehension#why-does-a-tuple-i-item-raise-an-exception-when-the-addition-works
Этот += в питончике реально странный
a = ['a', 'b', 'c']
a += 'wtf'
print(a) # ['a', 'b', 'c', 'w', 't', 'f']
a = a + 'wtf' # TypeError: can only concatenate list (not "str") to list
Почему так понятно, но сильно неожиданно когда ждешь что a=a+something и a+=something должно быть вроде как одно и то же. Т.е. по факту += совершенно самостоятельная операция а никакой не оператор присваивания. И в примере автора (и в доках) ни что иное как попытка создателей натянуть сову на глобус называя баг фичей.
А что вас удивляет? a += b
и есть совершенно самостоятельная операция — __iadd__
. Это же ни C# какой-нибудь, где a += b
— (практически[1]) полностью эквивалентно a = a + b
. То, что для удобства она разворачивается в a = a + b
, когда не определена явно — вот это и есть фича.
[1] За исключеним того, что выражение a
вычисляется только один раз, а не два.
+
и +=
для некоторых типов могут иметь разную семантику. Так как во многих языках
a += b
как-правило является эквивалентом a = a + b
, поведение этих операторов должно бы быть консистентным.В Python же для списка реализация оператора
a += b
эквивалентна вызову метода a.extend(b)
(за исключением присваивания), что описано в документации [1], который работает не только со списками, а с любым iterable.[1] docs.python.org/3/library/stdtypes.html#mutable-sequence-types
Почему список в кортеже ведет себя странно в Python?