Part 5

Ссылки

До сих пор мы представляли переменную как своего рода "коробку", которая содержит значение переменной. Технически в Python это не так. В переменной хранится не само значение, а ссылка на объект, который и является фактическим значением переменной. Объектом может быть, например, число, строка или список.

На практике это означает, что значение переменной не хранится в самой переменной. Вместо этого в ней есть информация о месте в памяти компьютера, где это значение можно найти.

Ссылку часто изображают стрелкой от переменной к фактическому значению в памяти:

5 2 1

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

a = [1, 2, 3]
print(id(a))
b = "This is a reference, too"
print(id(b))
Пример вывода

4538357072 4537788912

Ссылка, или ID переменной, — это целое число, которое можно представить как адрес в памяти компьютера, где хранится значение переменной. Если вы выполните код выше на своем компьютере, результат, скорее всего, будет другим, потому что ваши переменные будут указывать на другие места — ссылки будут другими.

Инструмент визуализации Python Tutor также показывает ссылки стрелками от переменной к фактическому содержимому, как мы видели в предыдущем разделе. Однако со строками инструмент немного "хитрит". Он показывает строки так, будто они хранятся в самих переменных:

5 2 2

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

Многие встроенные типы в Python, такие как str, являются неизменяемыми. Это означает, что значение объекта или любая его часть не может измениться. Значение можно заменить новым значением:

5 2 3

Некоторые типы Python являются изменяемыми. Например, содержимое списка может измениться без необходимости создавать совершенно новый список:

5 2 4

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

number = 1
number = 2
number += 10

Может показаться, что команды выше просто меняют значение, хранящееся в переменной, но на самом деле каждая команда создает в памяти компьютера совершенно новое число.

Вывод следующей программы проясняет ситуацию:

number = 1
print(id(number))
number += 10
print(id(number))
a = 1
print(id(a))
Пример вывода

4535856912 4535856944 4535856912

Сначала переменная number указывает на место в памяти 4535856912. Когда переменной number присваивается новое значение, она указывает на место 4535856944. Теперь, когда переменной a присваивается значение 1, a указывает на то же самое место, куда указывала number, когда ей тоже было присвоено значение 1.

Похоже, Python сохранил значение 1 в месте памяти 4535856912. Каждый раз, когда переменной присваивается значение 1, она ссылается на это место в памяти компьютера.

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

Несколько ссылок на один и тот же список

Что на самом деле происходит, когда вы присваиваете списковую переменную новой переменной — копируется ли список?

a = [1, 2, 3]
b = a
b[0] = 10

Присваивание b = a копирует значение, хранящееся в переменной a, в переменную b. Однако значение, хранящееся в a, — это не сам список, а ссылка на список.

То есть присваивание b = a копирует ссылку. В результате теперь есть две ссылки на одно и то же место в памяти, содержащее список.

5 2 5

К списку можно обратиться через любую из двух ссылок:

list1 = [1, 2, 3, 4]
list2 = list1

list1[0] = 10
list2[1] = 20

print(list1)
print(list2)
Пример вывода

[10, 20, 3, 4] [10, 20, 3, 4]

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

Инструмент визуализации снова очень полезен для понимания происходящего:

5 2 6

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

Если вы хотите создать настоящую отдельную копию списка, можно создать новый список и по очереди добавить в него каждый элемент исходного списка:

my_list = [1, 2, 3, 3, 5]

new_list = []
for item in my_list:
    new_list.append(item)

new_list[0] = 10
new_list.append(6)
print("the original:", my_list)
print("the copy:", new_list)
Пример вывода

my_list [1, 2, 3, 3, 5] new_list [10, 2, 3, 3, 5, 6]

Снимок процесса копирования в инструменте визуализации:

5 2 7

Переменная new_list указывает на другой список, чем переменная my_list.

Более простой способ скопировать список — оператор квадратных скобок [], который мы ранее использовали для срезов. Запись [:] выбирает все элементы коллекции. В качестве побочного эффекта она создает копию списка:

my_list = [1,2,3,4]
new_list = my_list[:]

my_list[0] = 10
new_list[1] = 20

print(my_list)
print(new_list)
Пример вывода

[10, 2, 3, 4] [1, 20, 3, 4]

Использование списков как параметров функций

Когда вы передаете список как аргумент функции, вы передаете ссылку на этот список. Это означает, что функция может напрямую изменять список.

Следующая функция принимает список как аргумент и добавляет новый элемент в конец списка:

def add_item(my_list: list):
    new_item = 10
    my_list.append(new_item)

a_list = [1,2,3]
print(a_list)
add_item(a_list)
print(a_list)
Пример вывода
[1, 2, 3] [1, 2, 3, 10]

Обратите внимание, что функция add_item не имеет возвращаемого значения. Она только изменяет список, который получает как аргумент.

Инструмент визуализации может помочь понять, что здесь происходит:

5 2 8

Global frame относится к переменным, определенным в главной функции, а фрейм add_item с синим фоном представляет параметры и переменные внутри этой функции. Как видно из визуализации, функция add_item ссылается на тот же самый список, что и главная функция. Изменения, сделанные внутри функции add_item, также влияют на главную функцию.

Другой способ реализовать эту функциональность — создать новый список внутри функции и вернуть его:

def add_item(my_list: list) -> list:
    new_item = 10
    my_list_copy = my_list[:]
    my_list_copy.append(new_item)
    return my_list_copy

numbers = [1, 2, 3]
numbers2 = add_item(numbers)

print("original list:", numbers)
print("new list:", numbers2)
Пример вывода

original list: [1, 2, 3] new list: [1, 2, 3, 10]

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

Редактирование списка, переданного как аргумент

Ниже попытка написать функцию, которая должна увеличить каждый элемент списка на десять:

def augment_all(my_list: list):
    new_list = []
    for item in my_list:
        new_list.append(item + 10)
    my_list = new_list

numbers = [1, 2, 3]
print("in the beginning:", numbers)
augment_all(numbers)
print("after the function is executed:", numbers)
Пример вывода

in the beginning: [1, 2, 3] after the function is executed: [1, 2, 3]

По какой-то причине функция не работает, так что же происходит?

Функция получает ссылку на список как аргумент. Она хранится в переменной my_list. Присваивание my_list = new_list присваивает новое значение той же переменной. Теперь переменная my_list указывает на новый список, созданный внутри функции, а ссылка на исходный список внутри функции больше недоступна. Однако за пределами функции это присваивание не имеет эффекта.

Кроме того, переменная new_list, содержащая новые увеличенные значения, недоступна извне функции. Она "теряется", когда выполнение функции заканчивается и управление возвращается в главную функцию. Переменная numbers в главной функции все время указывает на исходный список.

Инструмент визуализации и здесь ваш помощник. Аккуратно пройдите по этапам и посмотрите, как исходный список вообще не меняется при выполнении функции:

5 2 10

Один способ исправить это — скопировать все элементы из нового списка в старый список, по одному:

def augment_all(my_list: list):
    new_list = []
    for item in my_list:
        new_list.append(item + 10)

    # copy items from the new list into the old list
    for i in range(len(my_list)):
        my_list[i] = new_list[i]

В Python также есть удобная сокращенная запись для присваивания сразу нескольких элементов коллекции:

>>> my_list = [1, 2, 3, 4]
>>> my_list[1:3] = [10, 20]
>>> my_list
[1, 10, 20, 4]

В примере выше срезу списка присваиваются значения из другой коллекции.

Как мы знаем, срез может включать всю коллекцию:

>>> my_list = [1, 2, 3, 4]
>>> my_list[:] = [100, 99, 98, 97]
>>> my_list
[100, 99, 98, 97]

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

def augment_all(my_list: list):
    new_list = []
    for item in my_list:
        new_list.append(item + 10)

    my_list[:] = new_list

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

def augment_all(my_list: list):
    for i in range(len(my_list)):
        my_list[i] += 10
Loading
Loading
Loading
Loading
Loading
Loading

Побочные эффекты функций

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

Посмотрим на функцию, которая должна находить второй наименьший элемент списка:

def second_smallest(my_list: list) -> int:
    # in an ordered list, the second smallest item is at index 1
    my_list.sort()
    return my_list[1]

numbers = [1, 4, 2, 5, 3, 6, 4, 7]
print(second_smallest(numbers))
print(numbers)
Пример вывода
2 [1, 2, 3, 4, 4, 5, 6, 7]

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

Мы можем избежать побочного эффекта, немного изменив функцию:

def second_smallest(my_list: list) -> int:
    list_copy = sorted(my_list)
    return list_copy[1]

numbers = [1, 4, 2, 5, 3, 6, 4, 7]
print(second_smallest(numbers))
print(numbers)
Пример вывода

2 [1, 4, 2, 5, 3, 6, 4, 7]

Функция sorted возвращает новую отсортированную копию списка, поэтому поиск второго наименьшего элемента больше не нарушает порядок исходного списка.

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

Функции без побочных эффектов также называют чистыми функциями. Особенно при следовании функциональному стилю программирования это распространенный идеал. Мы подробнее рассмотрим эту тему в Advanced Course in Programming, который следует за этим курсом.

Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.

В этой части:
  1. 1. Дополнительные списки

  2. 2. Ссылки

  3. 3. Словарь

  4. 4. Кортеж