Зарегистрируйтесь, чтобы продолжить обучение

Ссылки Python: Списки

В Python в отличие от других языков, нет разделения на примитивные типы и ссылочные. Все всегда передается по ссылке. Почему об этом нужно знать?

С точки зрения прикладного программиста, разница проявляется при изменении данных, их передаче и возврате из функций. Нужно держать в уме, что списки как коллекции хранят в себе не сами значения, а ссылки на них.

При этом списки сами тоже передаются по ссылкам. Чтобы убедиться в этом, создадим несколько переменных, содержащих один список, и посмотрим, как они меняются:

items = [1, 2]


items2 = items
items2[0] = "python"

print(items2) #=> ["python", 2]
print(items) #=> ["python", 2]

В примере выше мы создаем новую переменную items2 и записываем в нее ссылку на переменную items. Теперь две переменные ссылаются на один и тот же список. А значит изменив список в любой из переменных, он поменяется и для другой.

Ссылки

Что же вообще такое "ссылки"? Ссылка это уникальный идентификатор объекта, условный адрес в виртуальной памяти интерпретатора, по которому хранится значение переменной. Получить этот адрес можно функцией id()

a = 42
id(a) # 138620829632016

Идентификатор — это обычное число. Но у каждого объекта свой уникальный идентификатор. Поэтому идентификаторы удобно использовать, чтобы отслеживать передачи ссылок на объект между разными участками кода — идентификатор объекта будет одним и тем же, по какой бы ссылке мы к объекту ни обращались:

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.

Проверкой is в Python пользуются, когда мы имеем дело с так называемыми объектами-одиночками. Самые известные одиночки в Python, это True, False и None. Поэтому проверка на равенство None обычно пишется так:

...
if foo is None:
    ...

Сравнение списков

Оператор == сравнивает списки, и любые другие объекты, по значению. То есть два списка будут равны, если имеют одинаковые значения:

[1, 2, 3] == [1, 2, 3] # True

Списки также можно сравнивать и по ссылке.

items = [1, 2, 3]
items2 = [1, 2, 3]

print(items2 == items) # True
print(items2 is items) # False

В этом примере, хоть списки и содержат одинаковые значения, но каждый список ссылается на свой адрес в виртуальной памяти.

Проектирование функций

Если передать список в какую-то функцию, которая его изменяет, то список тоже изменится. Ведь в функцию передается именно ссылка на список. Посмотрите на пример:

def append_wow(some_list):
  some_list.append('wow')

items = ['one']
append_wow(items)
print(items) # => ['one', 'wow']
append_wow(items)
print(items) # => ['one', 'wow', 'wow']

Проектируя функции, работающие со списками, есть два пути: менять исходный список или формировать внутри новый и возвращать его наружу. Какой лучше? В подавляющем большинстве случаев стоит предпочитать второй. Это безопасно. Функции, возвращающие новые значения, удобнее в работе, а поведение программы становится в целом более предсказуемым, так как отсутствуют неконтролируемые изменения данных.

Изменение списка может повлечь за собой неожиданные эффекты. Представьте себе функцию last(), которая извлекает последний элемент из списка. Она могла бы быть написана так:

def last(items):
  # Метод .pop() извлекает последний элемент из списка
  # Он изменяет список, удаляя оттуда этот элемент
  return items.pop()

items = [1, 2, 3]

last_item = last(items)
print(last_item) # 3
print(items) # [1, 2]

Где-то в коде вы просто хотели посмотреть последний элемент. А в дополнение к этому, функция для извлечения этого элемента взяла и удалила его оттуда. Это поведение очень неожиданно для подобной функции. Оно противоречит большому количеству принципов построения хорошего кода (например "Command–query separation", этот принцип рассматривается в курсе по функциям). Правильная реализация данной функции выглядит так:

# Список не изменяется
# Индекс -1 означает первый элемент с конца
def last(items):
  return items[-1]

items = [1, 2, 3]

print(last(items)) # => 3
print(items) # => [1, 2, 3]

В каких же случаях стоит менять сам список? Есть ровно одна причина, по которой так делают – производительность. Именно поэтому некоторые встроенные методы списков меняют их, например reverse() или sort():

items = [3, 2, 1, 5, 4]
items.sort()
print(items) # => [1, 2, 3, 4, 5]

items.reverse()
print(items) # => [5, 4, 3, 2, 1]

Копирование списков

В Python нет встроенных методов или функций, которые изменяют список, но возвращают новый, не трогая старый. Чтобы изменить список, не затрагивая изначальный, его нужно скопировать.

def append(items, item):

  # Или items_copy = items[:]
  items_copy = items.copy()
  items_copy.append(item)

  return items_copy

items = [1, 2, 3]
items2 = append(items, 4)

print(items) # => [1, 2, 3]
print(items2) # => [1, 2, 3, 4]

Несмотря на то, что подход, меняющий списки напрямую, сложнее в отладке, его используют в некоторых языках для увеличения эффективности работы. Если список достаточно большой, то полное копирование окажется дорогой операцией. В реальной жизни (веб-разработчика) это почти никогда не является проблемой, но знать об этом полезно.

Выводы

Все типы данных в Python передаются по ссылкам. Списки содержат коллекцию не значений, а ссылок.

При передаче списка в функцию передается ссылка на него. Изменение списка внутри функции повлияет на оригинал. Предпочтительнее возвращать новый список из функции, чтобы избежать неожиданных изменений.


Дополнительные материалы

  1. Продуманная оптимизация

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

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

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

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 26 декабря
профессия
от 24 542 ₸ в месяц
новый
Сбор, анализ и интерпретация данных
9 месяцев
с нуля
Старт 26 декабря

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

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

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

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