На прошлом уроке мы ввели понятие «ссылки» и упомянули, что в Python все и всегда передается по ссылке. Поэкспериментируем со списком, как с первым известным нам изменяемым объектом.
Но для начала нужно узнать о паре полезных для наших экспериментов инструментов — функции id
и операторе is
.
id
и is
Если обратиться к описанию функции (help(id)
), то документация скажет:
id(obj, /)
Return the identity of an object.
This is guaranteed to be unique among simultaneously existing objects.
Функция id
возвращает уникальный идентификатор объекта, который вы ей передаете в качестве аргумента и по ссылке.
Идентификатор — это обычное число. Но каждый отдельный объект имеет уникальный идентификатор, то есть любые два разных объекта всегда будут иметь отличающиеся идентификаторы. И пусть идентификаторы не сохраняются от одного запуска Python к другому, но в рамках одного запуска связь объекта и идентификатора нерушима.
Поэтому идентификаторы удобно использовать, чтобы отслеживать передачи ссылок на объект между разными участками кода — идентификатор объекта будет одним и тем же, по какой бы ссылке мы к объекту ни обращались:
a = "some string"
b = a
id(a) # 139739990935280
id(b) # 139739990935280
print(a is b) # => True
Когда мы присваиваем значение одной переменной другой, фактически создается новая именованная ссылка на исходное значение. Поэтому id(a)
и id(b)
возвращают одинаковый результат.
Оператор is
проверяет равенство идентификаторов своих операндов. В этом примере обе переменные ссылаются на один объект, поэтому проверка a is b
дает True
.
Проверка на равенство идентификаторов — очень быстрая. И особенно удобно ей пользоваться, когда мы имеем дело с так называемыми объектами-одиночками.
Самые известные одиночки в Python, это True
, False
и None
. Поэтому проверка на равенство None
обычно пишется так:
...
if foo is None:
...
Списки, кортежи и ссылки
Посмотрите на этот пример:
a = [1, 2, 3]
b = a
a.append(4)
print(b) # => [1, 2, 3, 4]
Что мы видим — поменяли "список a
", а изменился еще и "список b
". В действительности нет никаких двух списков, но есть две ссылки на один. Продолжим:
a = []
l = [a, a]
a.append(1)
print(l) # => [[1], [1]]
Здесь, как вы могли догадаться, в списке хранятся две ссылки на один и тот же объект — изменяемый к тому же. Именно в этом состоит тонкость работы со ссылками: когда мы получаем откуда-то ссылку, мы не можем быть уверены, что объект не будет меняться со временем без нашего участия.
Помните, мы говорили, что кортеж не может изменяться? Посмотрим на такой пример:
a = []
pair = (a, a)
pair[0].append(1)
pair[1].append(2)
print(pair) # => ([1, 2], [1, 2])
Как видите, значение в кортеже поменялось. Так произошло, потому что настоящее содержимое кортежа — это ссылки на значения. Эти ссылки меняться не могут, но вот сами объекты по этим ссылкам вполне могут поменяться.
Еще интереснее наблюдать за списками и кортежами, создаваемыми с помощью специального синтаксиса — умножения списка или кортежа на число.
Если умножить список или кортеж на число n
, то мы получим новую коллекцию соответствующего типа, состоящую из n
повторов элементов исходной коллекции. Вот несколько примеров:
print([1, 2, 3] * 3) # => [1, 2, 3, 1, 2, 3, 1, 2, 3]
print(('foo', 'bar') * 2) # => ('foo', 'bar', 'foo', 'bar')
print([[]] * 5) # => [[], [], [], [], []]
print(((),) * 5) # => ((), (), (), (), ())
Коллекции в Python это всегда коллекции ссылок. Можно догадаться, как такие растиражированные коллекции будут себя вести при изменении изменяемых элементов. Смотрите:
t = ([], [], []) * 3
print(t) # => ([], [], [], [], [], [], [], [], [])
t[0].append(42)
t[1].append(0)
print(t) # => ([42], [0], [], [42], [0], [], [42], [0], [])
Ссылки и присваивание
Мы увидели, что в список можно добавить несколько ссылок на один объект. И что переменные — те же ссылки, просто именованные.
Но что происходит с переменными и элементами списка при присваивании? Посмотрим:
a = "foo"
id(a) # 139739990954536
a += "bar"
print(a) # => 'foobar'
id(a) # 139739952783688
Этот пример показывает, что имя переменной не жестко связано со ссылкой на значение. Присваивание переменной (+=
— это вид присваивания) может поменять одну ссылку на другую. Это свойство присуще и элементам списка:
a = "foo"
l = [a, a]
print(l[0] is l[1]) # => True
l[0] += "bar"
print(l) # => ['foobar', 'foo']
print(l[0] is l[1]) # => False
Здесь сначала два элемента списка ссылаются на одно значение. Но после присваивания нового значения первому элементу, связь элемента с изначальным значением разрывается, и под конец элементы ссылаются на разные значения.
Изменения по месту
Изменения списка без создания нового называются изменениями по месту. Изменение списка по месту может быть полезным при работе с большими объемами данных. С его помощью можно избежать создания дополнительных объектов и ускорить выполнение программы.
С изменением списка по месту нужно работать осторожно, чтобы не изменить список неожиданным образом. Особенно, если он используется в нескольких местах в программе.
Если мы хотим создать копию списка, чтобы избежать изменения оригинального списка, можно использовать метод copy()
или срез:
l = [1, 2, 3]
l_copy = l[:]
l[0] = 42
print(l) # => [42, 2, 3]
print(l_copy) # => [1, 2, 3]
При работе с многомерными списками, которые содержат другие списки, копирование может быть неочевидным. Например, если мы создадим список с ссылкой на другие списки, то при копировании этот список будет содержать ссылки на те же внутренние списки:
l = [[1, 2], [3, 4]]
l_copy = l.copy() # копируется внешний список, но не внутренние списки
l_copy[0][0] = 0
print(l) # => [[0, 2], [3, 4]]
print(l_copy) # => [[0, 2], [3, 4]]
Чтобы создать копию списка, который содержит вложенные списки, используем метод deepcopy()
из модуля copy:
import copy
l = [[1, 2], [3, 4]]
l_copy = copy.deepcopy(l) # создается полная копия списка со вложенными списками
l_copy[0][0] = 0
print(l) # => [[1, 2], [3, 4]]
print(l_copy) # => [[0, 2], [3, 4]]
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.