Python Управление Памятью

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/
frozenset
PySetObject 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])
3

Parent имеет ссылку на 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 действительно происходит именно так, а не иначе.

Приглашаю всех высказываться в Комментариях. Критику и обмен опытом одобряю и приветствую. В хороших комментариях сохраняю ссылку на сайт автора!

И не забывайте, пожалуйста, нажимать на кнопки социальных сетей, которые расположены под текстом каждой страницы сайта.
Python Управление ПамятьюПродолжение тут…

Deviz_13

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Проверка комментариев включена. Прежде чем Ваши комментарии будут опубликованы пройдет какое-то время.