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

Вот несколько самых частых вопросов по этой теме:

  • Как получить адрес памяти объекта Python или произвести обратную операцию?
  • Как Python собирает мусор?
  • Как Python оптимизирует использование памяти (каков механизм интернирования)?

Если вы пока не можете четко ответить на эти вопросы, не волнуйтесь.

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

Примечание: эта статья описывает механизмы широко используемой реализации Python — CPython. Другие реализации Python (PyPy, Jython и др.) могут иметь иные принципы действия.

Как получить адрес памяти объекта Python или произвести обратную операцию?

Это самый простой вопрос. В CPython мы можем использовать встроенную функцию id() для получения адреса памяти объекта:

>>> punk=2077
>>> id(punk)
4319088080

Как показано выше, функция id() может возвращать адрес памяти переменной punk в виде целого числа.

Как же выполнить обратную операцию?

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

Но иногда нам может понадобиться использовать его для отладки каких-то глубоко спрятанных ошибок или же нам просто может стать интересно, как это делается. Python — дружелюбный язык программирования. Он понимает наши потребности и предоставляет нам для этой операции следующий способ:

>>> punk=2077
>>> id(punk)
4319088080
>>> import _ctypes
>>> print(_ctypes.PyObj_FromPtr(4319088080))
2077

Как показано выше, мы можем использовать функцию PyObj_FromPtr(), которая предоставляется встроенным модулем _ctypes, чтобы получить значение объекта по его адресу памяти.

Как Python собирает мусор?

Независимо от того, какой язык программирования мы используем, нам нужен механизм сборки мусора. Пространство памяти не безгранично, и если некоторые объекты вам больше не нужны, вы должны знать способы их удаления. В случае некоторых языков, таких, как C, вам придется делать это вручную. К счастью, в Python есть механизм автоматической сборки мусора. Поэтому нам не нужно ломать над этим голову при создании ПО.

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

Например, мы определяем простой целочисленный объект “2077”, и на него ссылаются две переменные (punk и yang):

# 1 ref to 2077
>>> punk=2077

# 2 ref to 2077
>>> yang=punk

Мы видим, что эти две ссылки указывают на один и тот же адрес памяти:

>>> print(id(punk),id(yang))
4322118928 4322118928

Конечно, мы можем вывести объект по его адресу в памяти:

>>> print(_ctypes.PyObj_FromPtr(4322118928))
2077

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

>>> del yang
>>> print(_ctypes.PyObj_FromPtr(4322118928))
2077

Как показано выше, в памяти все еще есть место для 2077. Ведь у нас есть переменная punk, которая ссылается на него.

Теперь давайте удалим punk:

>>> del punk
>>> print(_ctypes.PyObj_FromPtr(4322118928))
266
>>> print(_ctypes.PyObj_FromPtr(4322118928))
6160
>>> print(_ctypes.PyObj_FromPtr(4322118928))
2900

Как показано выше, мы удалили последнюю ссылку punk на объект 2077, после чего Python освободил соответствующий адрес памяти. Обращаться к этому адресу снова небезопасно, так как мы не знаем, что будет выведено в итоге.

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

Как Python оптимизирует использование памяти (каков механизм интернирования)?

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

>>> a=256
>>> b=256
>>> a==b
True
>>> a is b
True
>>> c=257
>>> d=257
>>> c==d
True
>>> c is d
False




Результаты этого кода странные. Когда мы сравниваем значения a и b или c и d, они, конечно, равны. Но если мы используем оператор is для сравнения их адресов памяти, то увидим, что a и b ссылаются на один и тот же адрес, а c и d— нет. 😕

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

>>> id(a)
4335976680
>>> id(b)
4335976680
>>> id(c)
4351986128
>>> id(d)
4351987056




Как-то не очень верится, правда? Но a и b действительно ссылаются на одно и то же пространство памяти, а вот c и d— нет. Почему так?

Это результат работы механизма интернирования Python.

Для экономии времени и памяти Python всегда предварительно загружает все небольшие целые числа в диапазоне [-5, 256]. Когда объявляется новая целочисленная переменная в этом диапазоне, Python просто ссылается на кэшированное целое число и не создает никакого нового объекта. (3 факта о кэшировании целых чисел в Python)

Приведенное выше объяснение разрешило наше недоумение по поводу странного кода. Ведь сколько бы переменных мы ни создали, пока они ссылаются на целое число 256, которое находится в диапазоне [-5, 256], все они будут указывать на один и тот же адрес памяти кэшированного целого числа.

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

Основные выводы

  1. Для получения адреса памяти объекта Python используется функция id().
  2. Для удаления мусора из памяти в Python применяется метод подсчета ссылок.
  3. Для экономии времени и памяти в Python предусмотрен механизм интернирования.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Yang Zhou, Memory Management in Python: 3 Popular Interview Questions

Предыдущая статьяВ чем магия ожидаемых результатов?
Следующая статьяКак определить содержимое ZIP-файла без скачивания