пятница, 21 ноября 2008 г.

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

Питон - язык с динамической типизацией и встроенным менеджером управления памятью. Присутствует еще длинный ряд особенностей и достоинств, но сейчас мы поговорим не о них, а именно о 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.
- иначе обычно объект все же удалится, пусть не сразу и с некторыми ограничениями. Таких примеров - большинсво.

Если будет интересно - о слабых ссылках в следующей статье.

вторник, 2 сентября 2008 г.

SQLAlchemy vs SQLObject

Спросили меня, чем алхимия лучше второй альтернативы. 

Я и ответил...

-----------------------------------

Начнем с описания объекта.

В SQLObject нужно наследоваться от базового класса, внутри него создавая определения колонок и отношений. 
В SQLAlchemy используется разделение: обычный класс, к которому посредством маппера сопостовляется таблица базы данных и определяются отношения. NB: мапперов может быть много для разных целей, и это удобно. Например, "легкий" для отчетов и "тяжеловесный" для внесения изменений. Дополнительная выгода - меньше "магических" атрибутов класса, которые могут вызвать непонятные конфликты. Больше возможностей для настройки.
Свой конструктор, наконец, в котором можно делать необходимые операции для создания НОВОГО объекта, и который НЕ ВЫЗЫВАЕТСЯ при зачитывании из БД.
Описание таблицы очень похоже на CREATE STATEMENT, а mapper проецирует таблицу на пользовательский объект.

Отношения. Они не исчерпываются понятиями "один-к-одному", "один-ко-многим", "многие-ко-многим". В алхимии для классического примера "пользователь-почтовые адреса" можно задать отношение, берущее адрес по порядковому номеру (list-like). А можно и по имени почтового ящика (dict-like). Или вообще задать свой класс для коллекции. Много чего можно, при этом отношения выглядят как привычные контейнеры.

Сессии-запросы.
В SQLObject делаем настраиваем ГЛОБАЛЬНОЕ подключение к базе и делаем Person.select(...)
В алхимии создаем сессию, подключаем ее к БД (существует множество способов, равно как и типов сессий)
Потом у сессии запрашиваем query для нашего класса, и через него строим запрос. Язык запросов очень мощный и гибкий, о нем чуть позже.
Все объекты, полученные через сессию или созданные для нее, запоминаются. Все изменения запоминаются опять же. Когда приходит пора записать - один раз записываем изменения. Плюс очень мощная обработка транзакций (в рамках той же сессии, естественно). Транзакции на уровне ORM и на уровне DB.

Язык запросов. На нижнем уровне повторяет SQL, позволяя писать SELECT/UPDATE значительно более простым способом, чем "работой с типом str". Когда запросы идут от session.query - переходим на уровень выше. Из ORM можно спросить практически все, что позволяет SQL - оставаясь при этом в объектной модели. Главное - правильно мапперы настроить 

Наследование, создание новых типов колонок, connection pooling, различные способы внесения собственных hooks и прочее - присутствует, и арсенал очень богат. Но, похоже, это не тема для "вступительной статьи".


Я описывал сильные стороны алхимии, но если не требуется - легко можно редуцировать. Выйдет не сложнее, чем в SQLObject. Tutuorial для алхимии очень простой. Сила проекта - в правильно выбраных уровнях абстракции и возможности осуществить тонкую настройку практически в любом необходимом месте. Сначала все это не требуется (и даже не нужно знать, что такие возможности существуют). Но очень приятно, что для возникающих проблем уже есть решение. Приходилось упираться в ограничения собственного проекта, не позволяющего без существенной переделки сделать "то-то и это". Но в ограничения алхимии упирался очень редко, и в следующей версии они снимались.

P.S. Есть проект Elixir, напоминающий подход SQLObject, но базирующийся на SQLAlchemy. Может быть заинтересует, но я на него давно не смотрел.

P.P.S. Так получилось, что SQLAlchemy начал использовать еще в бородатой 0.1 версии. И наблюдаю за бурным развитием проекта. Сейчас уже далеко не то, что тогда, и разработчики идут правильной дорогой. Рад за них. При всей сложности задачи решается она очень хорошо, оставляя конкурентов далеко за бортом.