Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Ссылки и изменяемость Python: Списки

На прошлом уроке мы ввели понятие «ссылки» и упомянули, что в 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]]

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 6 300 ₽ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 28 марта
профессия
от 5 025 ₽ в месяц
новый
Сбор, анализ и интерпретация данных
9 месяцев
с нуля
Старт 28 марта

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»