Объекты, ссылки и память в Python

Обновлено:

1. Все данные в Python представлены объектами

Во время работы программы Python создаёт в оперативной памяти объекты.

Например:

number = 10
price = 99.5
message = 'Hello'
numbers = [10, 20, 30]

В оперативной памяти находятся:

  • объект целого числа 10;
  • объект вещественного числа 99.5;
  • объект строки 'Hello';
  • объект списка [10, 20, 30].

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

Переменная — это имя, связанное с определённым объектом.

Упрощённо эту связь можно представить стрелкой:

number ────────> объект 10

Или:

message ───────> объект 'Hello'

2. Переменная является ссылкой

Создадим переменную:

message = 'Hello'

Python:

  1. получает или создаёт объект строки 'Hello';
  2. создаёт имя message;
  3. связывает имя message с объектом строки.

Упрощённая схема:

message ───────> 'Hello'

Поэтому правильнее говорить:

Переменная message ссылается на объект строки 'Hello'.

Также можно сказать:

Имя message связано с объектом 'Hello'.


3. Функция id()

Встроенная функция id() возвращает идентификатор объекта.

Она принимает один аргумент:

id(object)

Пример:

message = 'Hello'

print(id(message))

Результатом будет целое число:

4317167728

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

Важно: id() возвращает идентификатор объекта, а не идентификатор имени или переменной.

В строке:

id(message)

функция получает объект, на который сейчас ссылается имя message.

Идентификатор объекта гарантированно остаётся постоянным и уникальным среди одновременно существующих объектов в течение жизни этого объекта. В реализации CPython, которой обычно пользуются начинающие, значение id() является адресом объекта в памяти, но это деталь реализации, а не общее правило для всех реализаций Python. (Python documentation)


4. id() возвращает int

Результат функции id() является целым числом:

message = 'Hello'

object_id = id(message)

print(object_id)

Пример результата:

4317167728

В переменной object_id находится объект класса int.

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


5. id() принимает один аргумент

Правильно:

id('Hello')

Неправильно вызывать функцию без аргументов:

id()

Будет ошибка:

TypeError

Неправильно передавать несколько аргументов:

id('Hello', 'World')

Это тоже приведёт к ошибке TypeError.


6. Повторный вызов id() для одного объекта

Создадим переменную в IDLE:

message = 'Hello'

Несколько раз вызовем:

id(message)

Пока имя message ссылается на тот же объект, результат будет одинаковым:

4317167728
4317167728
4317167728

Объект продолжает существовать в текущей сессии IDLE, а его идентификатор в течение его жизни не меняется.


7. Перезапуск IDLE

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

После перезапуска мы снова создадим строку:

message = 'Hello'

Теперь id(message) может вернуть другое число.

Это будет другой запуск Python и новый объект либо объект, полученный заново в новом процессе.

Сравнивать идентификаторы объектов из разных запусков программы обычно не имеет смысла.


8. id() в Python-файле

Создадим файл:

object_id.py

Добавим код:

message = 'Hello'

print(id(message))

Запустим его с помощью кнопки Play.

Пример результата:

4317167728

Во время обычного запуска файла происходит следующее:

  1. запускается новый процесс Python;
  2. создаётся объект строки;
  3. имя message связывается с объектом;
  4. функция id() возвращает его идентификатор;
  5. программа завершается;
  6. операционная система освобождает ресурсы процесса.

9. Повторный запуск файла

При следующем запуске программа начинает работу заново:

message = 'Hello'

print(id(message))

Идентификатор часто будет другим:

4372572464

Но нельзя утверждать, что он обязательно будет отличаться при каждом запуске.

После уничтожения объекта его участок памяти может быть использован повторно. Поэтому два объекта, которые существовали в разное время, могут получить одинаковое значение id(). Python гарантирует уникальность идентификатора только в течение жизни объекта, а не между разными запусками программы. (Python documentation)


10. Конечная программа

Большинство небольших учебных файлов являются конечными программами.

У них есть:

  • начало выполнения;
  • набор команд;
  • завершение.

Пример:

numbers = [10, 20, 30]

result = sum(numbers)

print(result)

Программа:

  1. создаёт список;
  2. вычисляет сумму;
  3. выводит результат;
  4. завершает работу.

После завершения процесса его оперативная память освобождается.


11. Примеры конечных программ

Конечной может быть программа, которая:

  • преобразует один файл;
  • сжимает видео;
  • создаёт отчёт;
  • выполняет расчёт;
  • обрабатывает таблицу;
  • делает транскрипцию аудиозаписи;
  • собирает данные с сайта;
  • конвертирует изображения;
  • выполняет одну команду для криптобиржи.

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


12. Долгоживущие программы

Некоторые программы должны работать продолжительное время.

Например:

  • веб-сервер;
  • Telegram-бот;
  • торговый бот;
  • сервер компьютерной игры;
  • десктопное приложение;
  • программа, ожидающая действия пользователя.

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

Необязательно, что разработчик вручную написал именно while True. Цикл может находиться внутри используемой библиотеки или фреймворка.


13. Демонстрационный бесконечный цикл

Для демонстрации можно использовать готовый пример:

from time import sleep

message = 'Hello'

while True:
    print(id(message))
    sleep(1)

Этот код будет примерно раз в секунду выводить один и тот же идентификатор:

4317167728
4317167728
4317167728
4317167728

Пока программа работает и имя message ссылается на тот же объект, идентификатор объекта не меняется.


14. Важно о демонстрационном примере

В примере использованы конструкции:

from time import sleep
while True:

Импорты, значение True и цикл while сейчас подробно не изучаются.

Специально запоминать устройство этого кода пока не нужно.

Он нужен только для демонстрации того, что объект продолжает существовать, пока программа работает и на объект остаётся ссылка.


15. Идентификатор не является постоянным адресом навсегда

Пока объект существует:

message = 'Hello'

его id() не меняется.

Но идентификатор не закрепляется за строкой 'Hello' навсегда.

После завершения программы объект может быть удалён, а занятый участок памяти — использован другим объектом.

Поэтому правильно говорить:

Объект имеет постоянный идентификатор в течение своей жизни.

Неправильно:

Значение 'Hello' навсегда имеет один и тот же адрес.


16. Каскадное присваивание и один объект

Создадим три ссылки с помощью каскадного присваивания:

a = b = c = 'Hello'

Правая часть вычисляется один раз.

После этого три имени связываются с одним объектом:

a ──┐
b ──┼────> 'Hello'
c ──┘

Проверим:

print(id(a))
print(id(b))
print(id(c))

Все три идентификатора будут одинаковыми.

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


17. Отдельные одинаковые литералы

Рассмотрим другой пример:

a = 'Hello'
b = 'Hello'
c = 'Hello'

В CPython идентификаторы этих строк нередко совпадают.

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

Однако рассчитывать на такое поведение нельзя.

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

Даже для равных кортежей документация прямо допускает оба варианта: они могут оказаться одним объектом, а могут быть разными объектами. (Python documentation)


18. Неизменяемые объекты

К изученным неизменяемым типам относятся:

int
float
str
tuple

Неизменяемость означает, что после создания значение самого объекта нельзя изменить.

Например, невозможно изменить число 5 и превратить этот же объект в число 10.

Вместо этого имя связывается с другим объектом:

number = 5
number = 10

Упрощённо:

Сначала:

number ─────> 5

После перезаписи:

number ─────> 10

Старая связь с объектом 5 исчезла.


19. Повторное использование неизменяемых объектов

CPython применяет различные оптимизации и может повторно использовать некоторые неизменяемые объекты.

Особенно часто это заметно для:

  • небольших целых чисел;
  • некоторых строк;
  • констант внутри одного скомпилированного файла;
  • пустых кортежей.

Это позволяет не создавать лишние объекты в некоторых ситуациях.

Но это деталь реализации.

Нельзя строить логику программы на предположении:

Равные значения обязательно имеют одинаковый id.

Правильное правило:

Одинаковый id означает один объект.
Одинаковое значение не гарантирует одинаковый id.

20. Почему пример с 999 ненадёжен

Такой пример:

a = 999
b = 999

print(id(a))
print(id(b))

может показать как одинаковые, так и разные идентификаторы — в зависимости от версии Python, среды запуска и способа компиляции кода.

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

Для учебной демонстрации можно выполнить два преобразования во время работы программы:

a = int('999')
b = int('999')

print(id(a))
print(id(b))

В CPython обычно будут созданы два отдельных объекта, но полагаться на конкретную стратегию кэширования всё равно не следует.


21. Нужно ли самостоятельно избегать повторений

Если в программе уже есть значение:

PI = 3.14

лучше использовать существующую константу:

circle_length = 2 * PI * radius

а не повторять значение в разных местах:

circle_length = 2 * 3.14 * radius

Это важно прежде всего потому, что:

  • код проще читать;
  • значение проще изменить;
  • снижается вероятность ошибок;
  • исчезает дублирование;
  • становится понятен смысл числа.

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


22. Отдельно созданные списки

Создадим три одинаковых списка отдельными выражениями:

a = [1, 2, 3]
b = [1, 2, 3]
c = [1, 2, 3]

Каждое выражение со списком создаёт отдельный изменяемый объект.

Схема:

a ─────> [1, 2, 3]

b ─────> [1, 2, 3]

c ─────> [1, 2, 3]

Проверим:

print(id(a))
print(id(b))
print(id(c))

Идентификаторы будут различаться.


23. Почему отдельные списки являются разными объектами

Список — изменяемый объект.

Его содержимое можно изменить:

a.append(4)

После изменения:

print(a)
print(b)
print(c)

Результат:

[1, 2, 3, 4]
[1, 2, 3]
[1, 2, 3]

Изменился только первый список.

Второй и третий списки являются отдельными объектами.


24. Несколько ссылок на один список

Теперь используем каскадное присваивание:

a = b = c = [1, 2, 3]

В этом случае список создаётся один раз.

Все три имени ссылаются на один объект:

a ──┐
b ──┼────> [1, 2, 3]
c ──┘

Идентификаторы будут одинаковыми:

print(id(a))
print(id(b))
print(id(c))

25. Изменение общего списка

Добавим число через имя a:

a.append(4)

Выведем значения:

print(a)
print(b)
print(c)

Результат:

[1, 2, 3, 4]
[1, 2, 3, 4]
[1, 2, 3, 4]

Python не добавил число сразу в три списка.

В оперативной памяти существует только один список.

Имена a, b и c показывают на один и тот же изменённый объект.


26. Важное правило для изменяемых объектов

Эти варианты работают по-разному.

Три отдельных списка:

a = [1, 2, 3]
b = [1, 2, 3]
c = [1, 2, 3]

Один список и три ссылки:

a = b = c = [1, 2, 3]

Поэтому нельзя говорить:

Python всегда создаёт новый объект для изменяемого типа.

Правильнее:

Каждое отдельное выражение списка создаёт новый список, но несколько имён можно явно связать с одним и тем же списком.


27. Что содержит Python-объект

На уровне языка Python у каждого объекта есть три основные характеристики:

  • идентичность;
  • тип;
  • значение.

Идентичность можно исследовать с помощью:

id()

Тип определяет, каким является объект и какие операции он поддерживает:

int
float
str
list
tuple

Значение — это данные объекта:

5
'Hello'
[1, 2, 3]

Эти свойства являются частью модели данных Python. (Python documentation)


28. Как объект выглядит в CPython

В стандартной реализации CPython у базовой структуры объекта есть:

  • счётчик сильных ссылок;
  • указатель на объект типа.

Тип определяет поведение объекта и устройство его дополнительных данных.

Само значение хранится в части структуры, зависящей от конкретного типа.

Например:

  • целое число хранит данные числа;
  • строка хранит данные символов;
  • список хранит информацию о размере и ссылки на элементы;
  • кортеж хранит ссылки на свои элементы.

Для объектов переменного размера также используется информация о размере. Некоторые объекты дополнительно имеют служебные данные для сборщика циклического мусора, словаря атрибутов, слабых ссылок и других возможностей. Точная структура зависит от типа объекта, версии Python и сборки интерпретатора. (Python documentation)


29. Упрощённая схема объекта

Для начального понимания объект можно представить так:

Объект в оперативной памяти
┌──────────────────────────────┐
│ идентификатор                │
│ тип                          │
│ значение или данные объекта  │
│ счётчик ссылок в CPython     │
│ дополнительные данные типа   │
└──────────────────────────────┘

Это учебная схема.

В реальной структуре CPython идентификатор не хранится как отдельное поле: id() в CPython получает адрес объекта.

Значение также не находится в одном универсальном поле. Разные типы хранят свои данные по-разному.


30. Счётчик ссылок

CPython использует подсчёт ссылок.

У объекта есть счётчик, который показывает количество удерживающих его сильных ссылок на уровне реализации.

Когда появляется новая сильная ссылка, счётчик обычно увеличивается.

Когда ссылка исчезает, счётчик обычно уменьшается.

В базовом объекте CPython действительно находятся счётчик ссылок и указатель на тип объекта. (Python documentation)


31. Нельзя увидеть объект со счётчиком 0

Важно исправить распространённое упрощение.

Неправильная схема:

Объект продолжает лежать в памяти.
Ссылок стало 0.
Позже сборщик мусора обязательно найдёт его.

В обычном CPython, когда счётчик сильных ссылок объекта достигает нуля, объект, как правило, сразу уничтожается.

Поэтому получить доступ к обычному объекту и посмотреть у него счётчик 0 невозможно: ссылки на него уже не существует.

Отдельная работа сборщика циклического мусора нужна прежде всего для циклов ссылок, где счётчики объектов остаются больше нуля, хотя извне эти объекты уже недоступны. (Python documentation)


32. Удаление ссылки через del

Создадим объект:

numbers = [1, 2, 3]

Схема:

numbers ─────> [1, 2, 3]

Удалим имя:

del numbers

Ключевое слово del удаляет связь имени с объектом.

Оно не означает команду:

Немедленно физически удалить именно этот объект из оперативной памяти.

Если других ссылок нет, CPython обычно уничтожит объект после уменьшения счётчика до нуля.

Если другие ссылки существуют, объект продолжит жить.


33. del при нескольких ссылках

first = [1, 2, 3]
second = first

Схема:

first ──┐
        ├────> [1, 2, 3]
second ─┘

Удалим одно имя:

del first

Объект не исчезнет, потому что остаётся ссылка:

second ─────> [1, 2, 3]

К списку всё ещё можно обратиться:

print(second)

Результат:

[1, 2, 3]

34. Перезапись ссылки

Создадим переменную:

value = [1, 2, 3]

Сначала:

value ─────> [1, 2, 3]

Перезапишем переменную:

value = 'Hello'

Теперь:

value ─────> 'Hello'

Связь имени value со старым списком исчезла.

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


35. Завершение локальной области

В будущем мы будем создавать собственные функции.

Во время вызова функции появляются локальные имена.

Когда функция завершается, её локальные связи обычно освобождаются.

Если промежуточный объект больше нигде не используется, он может быть уничтожен.

Но объект продолжит существовать, если:

  • функция его вернула;
  • ссылка на него была сохранена в другом месте;
  • он входит в другой продолжающий существовать объект.

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


36. Сборщик мусора

Сборщик мусора по-английски называется:

garbage collector

В CPython управление памятью состоит из двух связанных механизмов:

  1. подсчёта ссылок;
  2. сборщика циклического мусора.

Подсчёт ссылок освобождает большинство объектов, когда их счётчик достигает нуля.

Сборщик циклического мусора дополняет этот механизм и ищет недоступные циклы ссылок. Это подсистема интерпретатора, а не отдельная запускаемая пользователем программа. (Python documentation)


37. Что такое цикл ссылок

Представим два изменяемых объекта:

Первый объект ссылается на второй.
Второй объект ссылается на первый.

Упрощённая схема:

объект A ─────> объект B
    ↑              │
    └──────────────┘

Даже если глобальные имена этих объектов удалить, внутри цикла ссылки останутся.

Счётчики объектов могут не стать равными нулю, хотя программа уже не способна обратиться к этим объектам обычным способом.

Именно для таких ситуаций нужен сборщик циклического мусора.


38. Как работает циклический сборщик

Упрощённо сборщик:

  1. отслеживает объекты-контейнеры, способные участвовать в циклах;
  2. периодически анализирует группы объектов;
  3. ищет циклы, недоступные из работающей программы;
  4. разрывает недоступные связи;
  5. позволяет освободить объекты.

Он не обязан каждый раз обходить абсолютно все объекты Python.

Для повышения эффективности объекты распределяются по поколениям или другим внутренним группам, а момент запуска зависит от количества выделений и освобождений памяти и от версии CPython.

Точные алгоритмы меняются между версиями, поэтому прикладной код не должен зависеть от конкретного момента запуска сборщика. (Python documentation)


39. Освобождение объекта и освобождение оперативной памяти

Даже если Python уничтожил объект, это не всегда означает, что занятые байты немедленно возвращены операционной системе.

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

Поэтому диспетчер задач не всегда сразу показывает уменьшение потребления памяти после удаления большого количества объектов.

Для программы эта память может быть свободна и готова к повторному использованию Python.


40. Другие реализации Python

Подсчёт ссылок и немедленное уничтожение объекта при достижении нуля относятся прежде всего к CPython.

Существуют другие реализации Python, например PyPy, которые могут использовать другие стратегии управления памятью.

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

Документация отдельно отмечает, что другие реализации Python могут управлять памятью иначе. (Python documentation)


41. Пример с x1 и x2

Рассмотрим код:

x1 = 5

x2 = x1

x1 += 5

print('x1 =', x1)
print('x2 =', x2)

Результат:

x1 = 10
x2 = 5

Разберём код по шагам.


42. Первый шаг

x1 = 5

Имя x1 связывается с объектом числа 5:

x1 ─────> 5

43. Второй шаг

x2 = x1

Python не помещает переменную x1 внутрь переменной x2.

Имя x2 получает ссылку на тот же объект:

x1 ──┐
     ├────> 5
x2 ──┘

На этом этапе:

print(id(x1))
print(id(x2))

покажет одинаковые идентификаторы.


44. Третий шаг

x1 += 5

Числа являются неизменяемыми объектами.

Объект 5 нельзя изменить и превратить в 10.

Поэтому операция работает по смыслу примерно так:

x1 = x1 + 5

Получается объект со значением 10, после чего имя x1 связывается с результатом.

Схема становится такой:

x1 ─────> 10

x2 ─────> 5

Фактический объект 10 может быть взят из внутреннего кэша CPython, поэтому не следует утверждать, что физически обязательно выделяется совершенно новый участок памяти. Главное — имя x1 теперь связано с объектом, представляющим значение 10.


45. Почему x2 не изменился

Имя x2 продолжает ссылаться на объект 5.

Перезапись имени x1 никак не изменяет связь имени x2.

Поэтому:

print('x1 =', x1)
print('x2 =', x2)

возвращает:

x1 = 10
x2 = 5

Так работают неизменяемые объекты.

Операция, создающая другое значение, не изменяет старый объект, а связывает имя с результатом операции. (Python documentation)


46. Сравнение со списком

Рассмотрим изменяемый список:

x1 = [1, 2, 3]
x2 = x1

x1.append(4)

print(x1)
print(x2)

Результат:

[1, 2, 3, 4]
[1, 2, 3, 4]

x1 и x2 ссылаются на один список.

Метод append() изменил сам объект, поэтому изменение видно через обе ссылки.


47. Перезапись списка работает иначе

x1 = [1, 2, 3]
x2 = x1

x1 = [4, 5, 6]

print(x1)
print(x2)

Результат:

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

В этом случае старый список не изменялся.

Имя x1 было связано с новым списком, а x2 осталось связано со старым.


48. Списки хранят ссылки на объекты

Рассмотрим:

number = int('1000')

numbers = [number, number]

Список не содержит две физические копии объекта 1000.

Он содержит две ссылки на один объект:

numbers
┌─────────────────────────┐
│ ссылка ─────> 1000      │
│ ссылка ─────> 1000      │
└─────────────────────────┘

Каждая позиция списка удерживает ссылку на соответствующий Python-объект.

C API Python возвращает элементы списков и кортежей именно как ссылки на объекты PyObject. (Python documentation)


49. Кортежи тоже хранят ссылки

number = int('1000')

numbers = (number, number)

Кортеж хранит две ссылки на один объект:

numbers
┌─────────────────────────┐
│ ссылка ─────> 1000      │
│ ссылка ─────> 1000      │
└─────────────────────────┘

Кортеж является неизменяемым контейнером, но его элементы также представлены ссылками на другие Python-объекты.


50. Контейнер удерживает объекты

Создадим список:

value = [1, 2, 3]

Добавим ссылку на него в другой список:

container = [value]

Теперь объект [1, 2, 3] удерживается как минимум:

  • именем value;
  • элементом списка container.

Удалим имя:

del value

Объект продолжит существовать, потому что ссылка на него остаётся внутри container.


51. Функция sys.getrefcount()

В модуле sys находится функция:

getrefcount()

Правильное название:

getrefcount

Не:

get_ref_count

Импортируем её:

from sys import getrefcount

Функция позволяет посмотреть текущий счётчик ссылок объекта в CPython.

Это диагностический инструмент. Использовать его для основной логики программы не следует.


52. Временная ссылка внутри getrefcount()

Рассмотрим:

from sys import getrefcount

numbers = [1, 2, 3]

print(getrefcount(numbers))

В обычном CPython часто получится:

2

Хотя мы создали только одно имя:

numbers

Дополнительная ссылка появляется из-за передачи объекта в саму функцию getrefcount().

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


53. Увеличение количества ссылок

from sys import getrefcount

numbers = [1, 2, 3]

print(getrefcount(numbers))

second_name = numbers

print(getrefcount(numbers))

Типичный результат в CPython:

2
3

После строки:

second_name = numbers

появилась ещё одна ссылка на тот же список.

Схема:

numbers ──────┐
              ├────> [1, 2, 3]
second_name ──┘

54. Ссылки внутри другого списка

Продолжим пример:

from sys import getrefcount

numbers = [1, 2, 3]
second_name = numbers

container = [numbers, numbers]

print(getrefcount(numbers))

Типичный результат:

5

Ссылки находятся:

  1. в имени numbers;
  2. в имени second_name;
  3. в первом элементе container;
  4. во втором элементе container;
  5. во временном аргументе getrefcount().

Важно: один и тот же объект можно положить в контейнер несколько раз. Каждая позиция удерживает отдельную ссылку.


55. Уменьшение счётчика через del

del second_name

print(getrefcount(numbers))

Типичный результат уменьшится на единицу:

4

Имя second_name больше не ссылается на список.

Но остаются:

  • имя numbers;
  • две ссылки внутри container;
  • временная ссылка функции getrefcount().

56. Уменьшение счётчика через перезапись

Перезапишем контейнер:

container = []

Старый список container, содержавший две ссылки на numbers, перестал быть доступен.

Обе ссылки из него освобождаются.

print(getrefcount(numbers))

Типичный результат:

2

Остались:

  • имя numbers;
  • временная ссылка getrefcount().

57. Полный пример с getrefcount()

from sys import getrefcount

numbers = [1, 2, 3]

print(getrefcount(numbers))

second_name = numbers

print(getrefcount(numbers))

container = [numbers, numbers]

print(getrefcount(numbers))

del second_name

print(getrefcount(numbers))

container = []

print(getrefcount(numbers))

Типичный результат в CPython:

2
3
5
4
2

Точные числа могут отличаться из-за версии Python, среды запуска, инструментов отладки и деталей реализации.

Для обучения важнее наблюдать не абсолютное число, а то, как счётчик увеличивается и уменьшается.


58. Передача объекта в функцию

Когда мы пишем:

numbers = [10, 20, 30]

result = sum(numbers)

функция sum() получает доступ к тому же списку.

Полная копия списка специально для sum() не создаётся.

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

Может использоваться временная удерживающая или заимствованная ссылка — точная работа зависит от реализации и механизма вызова. Поэтому нельзя гарантировать, что getrefcount() обязательно покажет ровно +1 при каждом вызове любой функции. (Python documentation)


59. Почему мы не видим ссылку после sum()

from sys import getrefcount

numbers = [10, 20, 30]

print(getrefcount(numbers))

result = sum(numbers)

print(getrefcount(numbers))

До и после sum() счётчик обычно будет одинаковым.

Ссылка, необходимая для вызова функции, существует только во время её работы.

К моменту выполнения следующей строки вызов уже завершён, и временные связи освобождены.

При этом функция вернула новый результат:

result

Переменная result ссылается на объект суммы, а не на исходный список.


60. Функция не всегда создаёт новую ссылку в счётчике

Для начального понимания можно говорить:

Функция временно получает ссылку на переданный объект.

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

Некоторые внутренние функции CPython используют заимствованные ссылки или оптимизированный механизм передачи аргументов.

Главная гарантия для нас:

  • объект не копируется автоматически;
  • функция работает с переданным объектом;
  • объект удерживается живым на время вызова.

61. Бессмертные объекты

В современных версиях CPython некоторые часто используемые объекты могут быть бессмертными.

Например, это может относиться к отдельным встроенным объектам и кэшированным значениям.

У них getrefcount() может показывать очень большое техническое число, которое не отражает обычное количество ссылок в учебном понимании.

Поэтому для экспериментов используем новый изменяемый объект:

numbers = [1, 2, 3]

а не:

number = 1

Документация sys.getrefcount() отдельно предупреждает, что для бессмертных объектов результат не соответствует обычному числу ссылок. (Python documentation)


62. getrefcount() нужен для исследования

Функция полезна, чтобы увидеть:

  • появление дополнительного имени;
  • добавление объекта в контейнер;
  • удаление имени через del;
  • исчезновение ссылок после перезаписи контейнера;
  • общий принцип подсчёта ссылок.

Она не предназначена для решений вида:

Если ссылок ровно три, выполнить определённое действие.

Счётчик зависит от внутренних деталей CPython и может меняться между версиями.


63. Разница между именем, ссылкой и объектом

Рассмотрим:

numbers = [1, 2, 3]

Здесь:

numbers

— имя.

Связь между именем и списком — ссылка.

[1, 2, 3]

— объект.

Упрощённо:

имя ───── ссылка ─────> объект

Обычно в разговорной речи говорят:

Переменная numbers ссылается на список.


64. Один объект может иметь много ссылок

numbers = [1, 2, 3]
first = numbers
second = numbers
container = [numbers, numbers]

Схема:

numbers ─────┐
first ───────┤
second ──────┼────> [1, 2, 3]
container[0] ┤
container[1] ┘

Все эти ссылки удерживают один список.

Пока существует хотя бы одна сильная ссылка, обычный объект продолжает существовать.


65. Одна ссылка указывает только на текущий объект

Имя не хранит историю своих предыдущих значений:

value = 10
value = 20
value = 'Hello'

После последней строки:

value ─────> 'Hello'

Имя value больше не предоставляет доступа к объектам 10 и 20.

Эти объекты могут продолжать существовать по внутренним причинам или благодаря другим ссылкам, но через имя value получить их уже нельзя.


66. Основные исправления распространённых утверждений

Неправильно:

id() всегда возвращает настоящий адрес памяти во всех версиях Python.

Правильно:

id() возвращает идентификатор объекта. В CPython это адрес объекта в памяти.

Неправильно:

При каждом новом запуске id() обязательно будет другим.

Правильно:

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

Неправильно:

Python всегда объединяет равные неизменяемые объекты.

Правильно:

CPython может повторно использовать некоторые неизменяемые объекты, но это не гарантируется.

Неправильно:

Python всегда создаёт отдельный изменяемый объект.

Правильно:

Отдельные выражения списков создают отдельные списки, но несколько имён можно связать с одним списком.

Неправильно:

Сборщик мусора ищет все объекты со счётчиком ссылок 0.

Правильно:

В CPython объекты со счётчиком 0 обычно уничтожаются сразу, а циклический сборщик ищет недоступные циклы с ненулевыми внутренними ссылками.


67. Основные правила урока

Функция:

id(object)

возвращает идентификатор объекта.

Идентификатор постоянен только в течение жизни объекта.

Переменная является именем, связанным с объектом:

имя ─────> объект

Каскадное присваивание создаёт несколько ссылок на один объект:

a = b = c = 'Hello'

Отдельные списки являются отдельными объектами:

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

Одна ссылка может быть скопирована:

b = a

Тогда обе переменные будут ссылаться на один объект.

Для неизменяемого числа:

x1 = 5
x2 = x1
x1 += 5

x1 начнёт ссылаться на 10, а x2 продолжит ссылаться на 5.

Списки и кортежи хранят ссылки на объекты, а не физические копии всех объектов.

del удаляет имя или другую указанную ссылку, а не обязательно сам объект.

В CPython основным механизмом является подсчёт ссылок, а циклический сборщик мусора дополняет его.

Функция:

from sys import getrefcount

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