Pull to refresh

Память и числа в Python

Reading time 3 min
Views 22K
Hi, All.

Ранее я работал с C-подобными языками, теперь же пришлось сесть за Python. Синтаксис дался легко, и наступил черёд каверзных вопросов. Под катом — статья про то как в Python реализовано хранение данных в памяти. Не претендую на истину, но пробую разобраться.

Смотрим на ссылки


Начнём с самого простого. Любые данные в Python это объект, любая переменная — ссылка на объект. Не существует никаких данных, которые не являются объектами. Для начала нам нужно научиться определять являются ли два «одинаковых» объекта одним и тем же. Для этого требуется получить адрес, что легко позволяет сделать встроенная функция id(). Пробуем:

print(id(0))

Как и ожидалось, выводится что-то невразумительное. Большое число, вероятно действительно адрес. Но если каждое число, использующееся на протяжении хранить в памяти, то никакой памяти, естественно, не хватит. Проводится короткий эксперимент:

print(id(0))
print(id(0))

Два абсолютно одинаковых числа. Следовательно, все константные числа на самом деле хранятся в памяти не дублираясь. Логично — у Python и так низкая производительность, такое ухищрение позволяет сохранить последние её остатки. Окей, попробуем заполнить всю память огромным массивом нулей.

a = [0]
while True:
    a += [0]

Бесконечный цикл, как и положено, выполняется бесконечно, но памяти практически не требует. Ещё один эксперимент:

a = [0, 0]
print(id(a[0]))
print(id(a[1]))

Ну да, одно и то же число. Скорее чтобы подтвердить провожу такую же проверку с двумя разными переменными — одно и то же число, да ещё и равное id(0). То есть, алгоритм, видимо такой: когда у нас меняется значение переменной мы проверяем нет ли в памяти такого же, и, если есть, перенаправляем ссылку на него. Это поведение требуется, очевидно, потому что объект занимает в памяти довольно много места, и, чтобы быть более компактным, Python по максимуму использует существующие объекты. Чтобы не загромождать статью кодом скажу что для строк (в том числе, полученных через срез), логических объектов и даже массивов это работает так же. Сделаем вторую попытку занять Python'ом всю память:

i = 0
a = [0]
while True:
    a += [a[i]]
    i += 1

Успех! Потребление памяти постоянно повышается. Делаем первый вывод:
1. Любые данные в Python это объекты.
2. Если объекты «одинаковы», то они хранятся по одному адресу в памяти. Иными словами, a == b и id(a) == id(b) это эквивалентные утверждения.
3. Более сложной оптимизации не используется — довольно простая зависимость в массиве уже не оптимизируется никак (только правило «a[i] = i»). Впрочем, я бы удивился, если бы использовалась: тут уже требуется довольно сложный лексический анализ, который Python с его пошаговой интерпретацией позволить себе не может.

Считаем ссылки


Disclaimer: сейчас мы будем работать в интерактивном режиме Python'а. Для того, чтобы подсчитать ссылки на объект есть функция sys.getrefcount(). Импортируем sys:

>>> from os import sys

И для начала нам нужно определить насколько реальными являются данные, которые она выдаёт:

>>> sys.getrefcount('There is no this string in Python')
3
>>> sys.getrefcount('9695c3716e3b801367b7eca6a3281ac9') #md5-хеш 512 рандомных байт из /dev/urandom.
3
>>> a = 'More random for the random god!'
>>> sys.getrefcount(a)
2
>>> a = 0
>>> sys.getrefcount(a)
434
>>> sys.getrefcount(0)
436

Это говорит нам об одной забавной вещи: считая ссылки, getrefcount() сам их создаёт. Как мы видим, для констант он создаёт их две штуки (действительно две, я пробовал на больших объёмах входных данных, которые здесь не публикую за ненадобностью), так что мы можем просто вычитать 2. На самом деле, видимо для переменных он тоже создаёт две, но не учитывает при этом саму переменную. Ну ладно, отклонениями результатов от реальности мы разобрались. Теперь несколько примеров:

>>> sys.getrefcount(1)
754
>>> sys.getrefcount(65)
13
>>> sys.getrefcount(67)
11
>>> sys.getrefcount('A')
4
>>> sys.getrefcount('a')
6
>>> sys.getrefcount(False)
100
>>> sys.getrefcount(True)
101
# Истина победила!

Почему тут внезапно появляются указатели на ровном месте (целыя 751 штука для единицы)? Потому что эта функция считает C-шные указатели, то есть включает те, которые используются в коде самого Python. Фактически, мы нагло вламываемся в ту часть Python, которую от нас стремятся скрыть разработчики.

Ну, вот такая вот закулиса у Python. Если дойдут руки и сумею — напишу о том, что произойдёт, если попробовать поменять эти объекты вручную через OllyDbg, скажем.
Tags:
Hubs:
-13
Comments 15
Comments Comments 15

Articles