Предполагается, что вы уже умеете писать простые программы на Python и знаете базовый синтаксис (if, for, функции, списки). Здесь мы разбираем не как пользоваться языком, а почему он работает именно так.
Попробуйте угадать, что выведет этот код.
x = 10
print(id(x))
x += 5
print(id(x))
Многие ожидают, что += просто изменит значение переменной.
Но если посмотреть на id(), окажется, что после выполнения операции объект стал другим.
Получается, что оператор += создал новый объект.
Теперь посмотрим почти такой же пример.
numbers = [1, 2]
print(id(numbers))
numbers += [3]
print(id(numbers))
На этот раз id() остаётся прежним.
Получается совершенно противоположное поведение.
В одном случае объект изменился.
В другом — нет.
Почему?
Если вспомнить предыдущие статьи, то ответ уже почти лежит на поверхности.
Мы знаем две важные вещи.
Во-первых, имя не хранит объект, а лишь ссылается на него.
Во-вторых, существуют изменяемые и неизменяемые объекты.
Именно второе свойство здесь оказывается решающим.
Начнём с числа.
x = 10
Имя x ссылается на объект со значением 10.
x ───► 10
Теперь выполняется операция:
x += 5
На первый взгляд кажется, будто число изменилось.
Но числа относятся к неизменяемым объектам.
Мы уже знаем, что изменить такой объект невозможно.
Поэтому Python делает совсем другое.
Он вычисляет результат:
10 + 5
создаёт новый объект со значением 15 и только после этого перенаправляет имя x на него.
Получается примерно такая последовательность:
x ───► 10
↓
создаётся объект 15
↓
x ───► 15
Старый объект при этом никак не изменяется.
Именно поэтому id() становится другим.
Теперь посмотрим на список.
numbers = [1, 2]
После этого выполняется:
numbers += [3]
Здесь многие ожидают аналогичного поведения.
Но список относится к изменяемым объектам.
А значит, Python может изменить существующий объект вместо создания нового.
Фактически происходит почти то же самое, что и здесь:
numbers.append(3)
или, если элементов несколько:
numbers.extend([3])
Сам список остаётся тем же самым объектом.
Меняется только его содержимое.
Поэтому id() не изменяется.
Чтобы увидеть разницу ещё лучше, сравним два примера.
С числами:
x = 10
before = id(x)
x += 5
after = id(x)
before и after окажутся разными.
Со списками:
numbers = [1, 2]
before = id(numbers)
numbers += [3]
after = id(numbers)
Здесь оба значения будут одинаковыми.
Получается интересная закономерность.
Оператор += сам по себе ничего не говорит о том, будет создан новый объект или нет.
Он лишь просит объект “прибавить” новое значение.
А дальше всё зависит от самого объекта.
Если объект можно изменить — Python изменяет его.
Если изменить нельзя — создаётся новый объект.
Именно поэтому одинаковый оператор ведёт себя по-разному.
Но есть одна интересная особенность.
Посмотрите на этот код:
numbers = [1, 2]
other = numbers
numbers += [3]
print(other)
Как вы думаете, что будет напечатано?
Если вы внимательно читали предыдущие статьи, то уже знаете ответ.
Получится:
[1, 2, 3]
Почему?
Потому что += изменил существующий объект.
Оба имени продолжали ссылаться на один и тот же список.
Теперь сравним с числами.
x = 10
y = x
x += 5
print(y)
Результат:
10
Потому что был создан новый объект, а имя x стало ссылаться именно на него.
Имя y продолжило ссылаться на прежний объект.
Теперь можно сформулировать правило, которое полезно запомнить.
Оператор += не гарантирует изменение существующего объекта.
И не гарантирует создание нового.
Он лишь вызывает поведение, которое определяет сам объект.
Для изменяемых объектов чаще всего происходит изменение “на месте”.
Для неизменяемых создаётся новый объект.
Если вам кажется, что это немного напоминает магию, не переживайте.
На самом деле никакой магии нет.
У каждого объекта есть собственные правила поведения.
Именно поэтому одинаковый оператор может выполнять совершенно разную работу.
И здесь возникает ещё один интересный вопрос.
Мы уже знаем, что объекты можно копировать.
Но что именно означает “копия”?
Если скопировать список, будут ли вложенные списки тоже скопированы?
И почему иногда изменение копии неожиданно меняет оригинал?
Об этом мы поговорим в следующей статье.