Python Управление Памятью — важнейшая особенность языка Python.
Пламенный привет посетителям этой страницы, пришедшим из социальных сетей, да и всем остальным тоже! В апреле 2021-го года наблюдал удивительное явление: обильный поток посетителей из 4-х социальных сетей. В связи с этим настоятельно рекомендую всем неоднократно и регулярно посещать сайт rtbsm.ru — там в общих чертах изложена Российская Теннисная Балльная Система Марии (Шараповой).
Приглашаю всех полюбоваться на Фото и Видео красавицы Марии — надеюсь, что Вы поделитесь адресом сайта rtbsm.ru с друзьями и знакомыми.
Главная проблема — известить Марию, чтобы она лично как можно скорее заявила на весь мир о РТБСМ.
Python Управление Памятью — тонкие механизмы языка Python, которые не всегда работают идеально чётко, поэтому нужно создавать программы так, чтобы не нарываться на лишние проблемы.
Автоматическое управление памятью
Управление памятью – это когда для каждой кучки данных в программе нужно руками выделить место в оперативной памяти. Следить, чтобы данные не вышли за пределы этого места. Не забыть освободить это место после того, как данные не нужны. В общем, адский геморрой.
В Питоне об этом думать не надо: язык программирования всё сделает за программиста. Правда, сделает неидеально: о том, как Питон работает с памятью, надо знать.
Интересную публикацию на тему Python Управление Памятью сделал Сергей Романов 16 сентября 2013 года ( https://habrahabr.ru/post/193890/ ) :
Использование памяти в Python
Сколько памяти занимает 1 миллион целых чисел?
Меня часто донимали размышления о том, насколько эффективно Python использует память по сравнению с другими языками программирования. Например, сколько памяти нужно, чтобы работать с 1 миллионом целых чисел? А с тем же количеством строк произвольной длины?
Как оказалось, в Python есть возможность получить необходимую информацию прямо из интерактивной консоли, не обращаясь к исходному коду на C (хотя, для верности, мы туда всё-таки заглянем).
Удовлетворив любопытство, мы залезем внутрь типов данных и узнаем, на что именно расходуется память.
Все примеры были сделаны в CPython версии 2.7.4 на 32 битной машине. В конце приведена таблица для потребности в памяти на 64 битной машине.
Форматы данных, которые нам потребуются.
символ Значение C Значение Python Длина на 32битной машине c char Строка из одного символа 1 i int int 4 l long int 4 L unsigned long int 4 d double float 8 Что в итоге?
Тип Имя в CPython формат Формат, для вложенных объектов Длина на 32 bit Длина на 64 bit Память для GC* Int PyIntObject LLl 12 24 float PyFloatObject LLd 16 24 str PyStringObject LLLli+c*(длина+1) 21+длина 37+длина unicode PyUnicodeObject LLLLlL L*(длина+1) 28+4*длина 52+4*длина tuple PyTupleObject LLL+L*длина 12+4*длина 24+8*длина Есть list PyListObject L*5 L*длину 20+4*длина 40+8*длина Есть Set/
frozensetPySetObject L*7+(lL)*8+lL LL* длина (<=5 элементов) 100
(>5 элементов) 100+8*длина(<=5 элементов) 200
(>5 элементов) 200+16*длинаЕсть dict PyDictObject L*7+(lLL)*8 lLL*длина (<=5 элементов) 124
(>5 элементов) 124+12*длина(<=5 элементов) 248
(>5 элементов) 248+24*длинаЕсть * Добавляет 12 байт на 32 битной машине и 32 байта на 64 битной машине
Мы видим, что простые типы данных в Python в два-три раза больше своих прототипов на C. Разница обусловлена необходимостью хранить количество ссылок на объект и указатель на его тип (содержимое макроса PyObject_HEAD).
Частично это компенсируется внутренним кэшированием, который позволяет повторно использовать ранее созданные объекты (это возможно только для неизменяемых типов).
Для строк и кортежей разница не такая значительная — добавляется некоторая постоянная величина.
А списки, словари и множества, как правило, занимают больше на 1/3, чем необходимо. Это обусловлено реализацией алгоритма добавления новых элементов, который приносит в жертву память ради экономии времени процессора.
Итак, отвечаем на вопрос в начале статьи: чтобы сохранить 1 миллион целых чисел нам потребуется 11.4 мегабайт (12*10^6 байт) на сами числа и дополнительно 3.8 мегабайт (12 + 4 + 4*10^6 байт) на кортеж, который будет хранить на них ссылки.
Подробности можете посмотреть по приведенному перед цитатой адресу.
Получается, что в среднем на каждое целое число уходит по 16-ть байт. При нынешних объёмах RAM, SSD и HDD можно без проблем оперировать миллионами чисел и не особо волноваться по поводу излишнего расхода памяти.
Привожу полезную публикацию о механизмах Python Управление Памятью, сделанную автором Андрей Светлов 21-го ноября 2008-го года ( asvetlov.blogspot.ru/2008/11/blog-post.html) :
Управление памятью в Питоне
Питон — язык с динамической типизацией и встроенным менеджером управления памятью. Присутствует ещё длинный ряд особенностей и достоинств, но сейчас мы поговорим не о них, а именно о memory management.
На первый взгляд всё просто: программист создаёт объект, а когда этот объект становится не нужен — он автоматически удаляется. Обычно так всё и происходит, но иногда система даёт сбой — память постоянно растёт, встроенный garbage collector не работает.
Программист впадает в лёгкую панику и начинает ругать сквозь зубы «чертов язык программирования» и самого Гвидо ван Россума, попутно стараясь разобраться в проблеме.
Чаще всего это заканчивается откатом исходников до версии «всё вроде бы хорошо» — без понимания настоящей причины.
На самом деле такой подход никуда не годится… На те же грабли можно наступить опять ненароком и снова потратить значительное время на решение. Да и вообще как-то не неловко — убежал, вместо того, чтобы осознать и победить.
Я попытаюсь показать, как работает этот механизм управления памятью в Питоне, и, главное, как он не работает. Чудеса — только в сказках.
Итак, с созданием объекта проблем никогда не бывает — только с его удалением. К слову, в Питоне всё — объекты. Классы, переменные, функции, модули, число 3.14 и строка ‘я — Андрей’. Исключений нет.
Проблемы бывают с удалением. Вернее, само по себе освобождение памяти тоже работает отлично. Беда в другом. Программист ожидает, что объект будет удалён — ведь он больше этому программисту не нужен.
А Питон думает иначе. В результате — конфликт интересов, Питон априори выигрывает. Программист получает головную боль.
Чтобы понять, почему эта ситуация иногда происходит, нужно углубиться в детали.
Исторически сложилось так, что в Питоне существуют два механизма для освобождения памяти — decref и garbage collector. Считаю это большим достоинством и чуть позже объясню почему.
Начнем с первого. Питон считает ссылки на объект. Это количество переменных, ссылающихся на него.
>>> import sys
>>> a = ‘Hello world’
>>> b = a
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(b)
3
>>> del a
>>> sys.getrefcount(b)
2Всегда отнимаем единицу от getrefcount — она автоматически добавляется при вызове функции.
Пока что всё просто. Когда удаляется ссылка — счётчик уменьшается на единицу. Когда он становится равным нулю — удаляется сам объект. Это — decref (по названию макроса в C API, делающего всю работу).
Теперь — более сложный пример. Дерево.
>>> class Parent(object):
… def __init__(self):
… self.children = []
… def add(self, ch):
… self.children.append(ch)
… ch.parent = self
…
>>> class Child(object):
… def __init__(self):
… self.parent = None
…
>>>
>>> p = Parent()
>>> p.add(Child())
>>> p
<__main__.parent>
>>> p.children
[<__main__.child>]
>>> sys.getrefcount(p)
3
>>> sys.getrefcount(p.children[0])
3Parent имеет ссылку на child, а тот в свою очередь — на родителя. Даже если мы удалим все внешние ссылки — они друг на друга всё ещё ссылаются, счётчик ссылок у каждого по единице (если добавили несколько child — у parent, соответствено, больше). Объекты остались в памяти, хотя программисту они уже не нужны — он «выбросил их и забыл». Проблема, думаю, ясна. Мусор.
Тогда появился второй способ — garbage collector (кажется, начиная с версии 2.1. Точно не помню, но помню, как ему радовался). Управление им — через модуль gc.
Коротко работу собирателя мусора можно описать так:
- есть три поколения объектов
- когда новый объект создается — сразу же попадает в первое поколение
- считается количество созданных/удаленных объектов
- если разница больше порога — запускается умный cycle finder
- если объект все еще не удален даже gc — он перемещается в более старое поколение
- если дело совсем худо — поймаем нашего нарушителя в gc.garbage
- всё настраивается — смотрите документацию по gc.
- gc предоставляет ещё много интересной информации, как-то: кто ссылается на объект и на кого он ссылается, кто попадает в garbage, список всех объектов, живущих в Питоне и т. д.
- детали на самом деле не очень важны.Cycle finder пытается найти cycle dependencies — циклические зависимости (я буду называть их кольцами) — и удалить их. Т. е. если ты ссылаешься на меня, а я на тебя — и никто на нас снаружи — мы попадем под garbage collector и нас успешно разыменуют.
Рано или поздно. Пиковое потребление памяти может быть довольно большим, но «в среднем по больнице температура 36.6″. В тяжёлых случаях можно принудительно запустить gc.collect() — но это выглядит как-то не кошерно.
Обычно всё работает и позволяет ничего не делать в случае parent-child. Проблемы возникают, когда один из объектов кольца имеет метод __del__ или написан как extension, т. е. не на Питоне. Второй случай замнём для ясности — хотя для меня он весьма актуален.
Вернемся к __del__. Очень полезный метод, позволяющий сделать «уборку за собой». Например, закрыть файл логов, отсоединиться от базы данных и т. д. Проблема в том, что garbage collector вычислил кольцо, в котором, возможно, есть несколько объектов с __del__.
И __del__ от parent может использовать свой child, который уже удалён — получим странную ошибку. В такие интимные детали garbage collector не вникает, просто помещает всё кольцо в мусорник — gc.garbage.
При этом оставляя программисту возможность посмотреть на это безобразие и разрулить ситуацию самому. Никогда такого не делал и считаю дурным тоном — мало ли кто в мусорник попадёт, а мне за всех отвечать…
Для решения сложных проблем с кольцами быстро появился ещё один стандартный модуль — weakref. Т. е. слабая ссылка, которая как бы видит другой объект, но при этом не увеличивает его счётчик.
Достоинства двойного способа удаления объектов:
- если Вы аккуратны и внимательны — получите минимальное использование памяти и явные вызовы __del__ aka destructor;
- иначе обычно объект всё же удалится, пусть не сразу и с некоторыми ограничениями. Таких примеров — большинство.
Приведенная цитата вводит в понимание процессов управления памятью и заставляет уделять этой проблеме некоторое внимание, чтобы быть готовым к возможным неприятностям из-за несовершенства механизмов освобождения памяти.
Интересную информацию можно почерпнуть в докладе, который сделал Валентин Синицын, разработчик сервиса Яндекс.Такси ( http://pycon.ru/2015/program/content/sinicin/ ) :
Python: управление памятью
Программистам на Python не нужно задумываться об управлении памятью — за них всё делает Интерпретатор. Как правило, это всё, что необходимо знать об управлении памятью в Python. Порой подобный «минимализм» приводит к неожиданным последствиям. Память начинает «течь», и понять, кто в этом виноват, оказывается непросто. В этом докладе мы сделаем обзор подсистемы управления памятью в самой популярной реализации Python — CPython. Будут рассмотрены процедуры выделения памяти и сборки мусора, способы взаимодействия с ними из Python-кода, а также типовые ошибки, которые могут помешать их нормальной работе. Мы также продемонстрируем серию простых экспериментов, доказывающих, что работа с памятью в Python действительно происходит именно так, а не иначе.
Приглашаю всех высказываться в Комментариях. Критику и обмен опытом одобряю и приветствую. В хороших комментариях сохраняю ссылку на сайт автора!
И не забывайте, пожалуйста, нажимать на кнопки социальных сетей, которые расположены под текстом каждой страницы сайта.
Продолжение тут…