Знаете ли вы, что += в Python это не просто сокращение x = x + y?
Выглядит как короткая запись – но под капотом это два разных оператора. x = x + y всегда создает новый объект и присваивает его переменной слева. += сначала пытается изменить сам существующий объект, не создавая новый – через метод __iadd__. И только если у типа такого метода нет – работает как обычный +: создает новый объект. Для изменяемых типов (list, bytearray) разница принципиальна.
Если на один список смотрят две переменные, две внешне одинаковые операции дают разный результат:
a = [1, 2, 3]
b = a
a += [4]
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3, 4] – b изменился, теперь это тот же список
a = [1, 2, 3]
b = a
a = a + [4]
print(a) # [1, 2, 3, 4]
print(b) # [1, 2, 3] – новый объект, b нетронут
Для списка += вызывает метод list.__iadd__ – он дописывает элементы в существующий объект и возвращает тот же самый объект. + всегда создает новый список. Поэтому когда на один список уже есть другая ссылка, результаты двух операций расходятся.
Еще одна ситуация – кортеж, внутри которого лежит изменяемый объект:
t = ([1, 2, 3],)
try:
t[0] += [4]
except TypeError as e:
print(e) # 'tuple' object does not support item assignment
print(t) # ([1, 2, 3, 4],) – список ВНУТРИ обновился
Python выполняет t[0] += [4] в два шага: сначала вызывает list.__iadd__ на внутреннем списке – список меняется и становится [1, 2, 3, 4]. Потом Python пытается записать результат обратно через t[0] = ... – и падает, потому что кортеж неизменяем. Операция и упала с ошибкой, и сработала одновременно.
Понять, создает ли += новый объект или меняет старый – можно через id():
x = 10
print(id(x))
x += 1
print(id(x)) # другой id – int неизменяем, += создал новый объект
lst = [1, 2]
print(id(lst))
lst += [3]
print(id(lst)) # тот же id – сам список поменялся, новый объект не создавался
Для неизменяемых типов (int, str, tuple, frozenset) += действительно эквивалентен x = x + y – создается новый объект и присваивается переменной. Для изменяемых – меняется тот же объект, к которому уже были привязаны и другие переменные. Отсюда и расхождение ссылок, и парадокс с кортежем.
+= это не синтаксический сахар, а отдельный оператор со своим dunder-методом __iadd__. Для изменяемых типов он меняет сам существующий объект, не создавая новый – из-за этого ломаются разделяемые ссылки и появляется парадокс с кортежем, в котором операция падает и срабатывает одновременно. То же самое относится к -=, *=, /= и остальным – у каждого свой __isub__, __imul__, __itruediv__.
В следующий вторник – * на списке: почему [[]] * 3 это не три копии, а три ссылки на один объект.
x
https://t.me/codebites