В 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 передаются по ссылкам. Списки содержат коллекцию не значений, а ссылок.
При передаче списка в функцию передается ссылка на него. Изменение списка внутри функции повлияет на оригинал. Предпочтительнее возвращать новый список из функции, чтобы избежать неожиданных изменений.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.