Управление памятью — одна из самых популярных тем, которые обсуждаются на собеседованиях для разработчиков 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 предусмотрены механизмы интернирования не только для малых чисел, но и для коротких строк.
Основные выводы
- Для получения адреса памяти объекта Python используется функция
id()
. - Для удаления мусора из памяти в Python применяется метод подсчета ссылок.
- Для экономии времени и памяти в Python предусмотрен механизм интернирования.
Читайте также:
- Рекурсия и цикл, в чем разница? На примере Python
- Собеседование: 8 самых распространенных ошибок программистов
- 5 впечатляющих возможностей Julia, которых не хватает в Python
Читайте нас в Telegram, VK и Яндекс.Дзен
Перевод статьи Yang Zhou, Memory Management in Python: 3 Popular Interview Questions