Записавшимся должно прийти на почту письмо с местом, временем и прочими деталями.
среда, 26 июня 2013 г.
Начало курсов «Learn Python»
Записавшимся должно прийти на почту письмо с местом, временем и прочими деталями.
пятница, 3 мая 2013 г.
Удаление объектов сборщиком мусора
На Хабре появилась толковая статья о неприятностях, которые может доставить Garbage Collector.
Отмечу актуальность описания проблемы и полную корректность решения для tornado.
Однако не могу не обратить внимание на слабое освещение того, как именно работает сборщик мусора в CPython.
Почему при работающем garbage collector появляется так много неудаляемых объектов?
Немного теории.
Сборщик мусора имеет три поколения (счёт начинается с нуля). При создании объекта он попадает в нулевое поколение.
У каждого поколения есть счётчик и порог. Работает эта пара так:
- При добавлении объекта в поколение счётчик увеличивается.
- При выбывании из поколения счётчик уменьшается.
- Когда счётчик превысит пороговое значение — по всем объектам из поколения пройдётся сборщик мусора. Кого найдёт — удалит.
- Все выжившие в поколении объекты перемещаются в следующее (из нулевого в первое, из первого во второе). Из второго поколения объекты никуда не попадают и остаются там навечно.
- Перемещённые в следующее поколение объекты меняют соответствующий счетчик, и операция может повториться уже для следующего поколения.
- Счётчик текущего поколения сбрасывается.
Объекты, подлежащие уничтожению но имеющие переопределённый метод
__del__
не могут быть собраны. Причина проста: эти объекты могут
ссылаться друг на друга.
Python не способен определить безопасный порядок вызова __del__
. Если
вызывать деструкторы в произвольном порядке, то можно получить
ситуацию вида:
- Деструктор объекта a для работы требует объект b.
- Последний в своём деструкторе обращается к объекту a.
- Если вызовем
__del__
у a, то деструктор b не сможет отработать нормально. Ссылка на a будет иметь значение None.
Чтобы не заставлять программиста корректно разрешать такие ситуации
было принято решение не уничтожать подобные объекты а просто
перемещать их в gc.garbage
— и дальше программист пусть сам
разбирается что делать с этим мусором.
К слову, в потоках-демонах тоже возникает ситуация подобная описанной выше, но там программист должен быть готов к тому что переменная внезапно стала None. Подробности смотрите здесь
Перейдём к практической части.
Пример
Для иллюстрации рассмотрим немного изменённый пример из приведенной в самом начале статьи.
Имеем классическую древовидную структуру:
class Node(object):
parent = None
def __init__(self, *children):
self.children = list(children)
for node in self.children:
node.parent = self
@classmethod
def tree(cls, depth=1, numchildren=1):
if depth == 0:
return []
return [cls(*cls.tree(depth-1, numchildren))
for _ in range(numchildren)]
Родитель и потомки напрямую связаны друг с другом циклической связью.
Метод tree
создает дерево нужной глубины.
Добавляем garbage collection hook для того чтобы увидеть когда срабатывает сборщик мусора и сколько объектов он уничтожает:
import gc
def gc_cb(phase, info):
if not info['collected'] and not info['uncollectable']:
return
print("{0}:\t{1[generation]}\t{1[collected]}\t{1[uncollectable]}".format(
phase, info))
gc.callbacks.append(gc_cb)
Наконец, делаем много-много наших деревьев и смотрим как они разрушаются:
for n in range(20):
for _ in range(n):
Node.tree(depth=5, numchildren=6)
Пороги стоят стандартные:
>>> gc.get_threshold()
(700, 10, 10)
700 объектов в нулевом поколении и по 10 в первом и во втором.
Анализ
Теперь о том, почему образуется столько мусора.
Пример напечатает что-то вроде такого (вырезка из очень длинного результата):
...
stop: 1, 4665, 0
stop: 2, 79305, 0
stop: 1, 4665, 0
stop: 2, 79305, 0
stop: 1, 4665, 0
stop: 1, 4665, 0
stop: 1, 4665, 0
stop: 2, 97965, 0
stop: 1, 4665, 0
stop: 2, 79305, 0
stop: 1, 4665, 0
...
За один вызов Node.tree(depth=5, numchildren=6)
создается 9330 тесно
связанных объектов, которые нельзя разрушить в 0 поколении (помним,
что порог 700). Значит они попадают в первое, а большая часть даже во
второе поколение (9330>700*10). Наконец все 9330 объекта созданы,
можно разрушать.
На уменьшении счётчиков ссылок на объекты ничего убрать не получится. Поэтому ждём, когда опять превысим порог в 700 (на следующем вызове Node.tree, конечно).
Собираем нулевое поколение (оно оказывается заполнено свежими данными и поживиться почти ничем не удаётся).
А сборщик мусора для поколения 1 вызовется только если туда попадут как минимум 10 объектов из поколения 0.
Хорошо, мы добрались до сбора в 1 поколении. Часть циклов можно уничтожить сразу (два поколения для анализа лучше одного), некоторые переправляются в поколение 2. В котором сборщик запускается тоже если в свою очередь превысили порог.
Что случается ещё реже и таким образом наши объекты накапливаются во втором поколении. Когда сборщик мусора доходит до него, то всё чистит.
Проблема в том, что до последнего поколения дело доходит относительно редко.
В результате имеем не слишком типичный для сборщика мусора случай.
Чиним
Конечно, лучше всего не доводить дело до сборщика мусора вообще в случаях подобным нашему синтетическому примеру.
Разрушать ссылки вручную через вызов del
или присваивания None
очень неудобно, но есть и другой способ.
Воспользуемся слабыми ссылками на родителя:
import weakref
class Node(object):
parent = None
def __init__(self, *children):
self.children = list(children)
for node in self.children:
node.parent = weakref.proxy(self)
Я предпочитаю weakref.ref
как дающий больший контроль (всегда можно
добавить свойство):
class Node(object):
_parent = None
def __init__(self, *children):
self.children = list(children)
for node in self.children:
node._parent = weakref.ref(self)
@property
def parent(self):
if self._parent is None:
return None
else:
return self._parent()
В любом варианте дело до сборщика мусора не дойдёт и объекты будут уничтожены сразу как только перестанут быть нужны.
Если вариант со слабыми ссылками почему-то не проходит можно просто увеличить пороги. У нас создаётся за раз 9330 объектов? Поставим порог для первого поколения в 10000.
gc.set_threshold(10000, 100, 100)
Результат выглядит куда лучше:
...
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 1, 919005, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
...
Как видим сборщик мусора уничтожает почти всё на первом проходе, и лишь иногда требуется второй. Правда, смущает цифра 919005.
Именно потому что не всё прибивается на первом проходе, а второй наступает нескоро.
Уменьшаем второй порог:
gc.set_threshold(10000, 10, 10)
Ага, теперь всё красиво:
...
stop: 1, 83970, 0
stop: 0, 9330, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 1, 74640, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
stop: 1, 102630, 0
stop: 0, 4665, 0
stop: 0, 4665, 0
...
Выводы
В результате всё просто. Используем слабые ссылки. Если это по каким-то причинам невозможно — поднимаем пороги.
Но при этом нужно помнить, что сборщик мусора будет запускаться реже.
Установка порогов в слишком большое значение способно в нашем случае съесть память не менее успешно, чем если бы эти значения оставались установленными по умолчанию.
вторник, 23 апреля 2013 г.
Новый набор на курсы Learn Python
Асинхронное программирование
Сетевые библиотеки, например twisted, tornado, gevent, tulip — при кажущейся разнице в подходах имеют очень похожее ядро, называемое reactor, io loop, hub, event loop соответственно. Именно созданием этого ядра с нуля своими руками мы и займемся.
Цель курса: дать знания о том, как происходит работа с сетевыми подключениями (сокетами) на примере создания собственной библиотеки.
Требования к участникам: знание Python на достаточно приличном уровне. Введения в программирование не будет, с другой стороны создаваемый код не потребует чего-то сложного. Все сложности будут в работе с сетью а не в создании хитрых питоновских конструкций.
Потребуется Python 3.3. Хотя код мало зависит от версии языка, всё же в Python 3.3 появились некоторые удобные штуки которыми мы воспользуемся.
Операционная система: Linux, MacOS X или FreeBSD на выбор. Если есть сильное желание писать на Windows — тоже можно.
Коротко о чём будут занятия:
- Написание примитивного клиент-серверного кода на потоках.
- Объяснение почему производительные программы такой подход не используют. Нужно делать на epoll или kqueue, в крайнем случае select. Создание своего event loop. Сначала для обработки отложенных событий. Что это такое и какой должен быть интерфейс — расскажу по ходу дела.
- Описание того, как работает select/epoll/kqueue. Добавляем к event loop работу с TCP сокетами, основанную на обратных вызовах. Правильная обработка ошибок.
- Добавляем понятия транспорта-протокола.
- Строим поверх этого удобный интерфейс для пользовательского кода. На yeild from или greenlets — по желанию.
- Окончательный разбор результатов, ответы на возникшие вопросы.
Получившийся код в целом будет в основе следовать дизайну tulip в сильно упрощённом виде.
Курс состоит из шести занятий. Лектор: Андрей Светлов
Каждое занятие длится 2 часа.
Стоимость занятия: 300 грн.
Создание эффективных web-приложений
В 2013м году никого не удивишь веб-приложением, построенным при помощи Django, Pyramid или даже Flask. Однако куда сложнее удивить грамотным и эффективным web-приложением, способным одинаково успешно справлятся с нагрузкой реального мира и оставаться простым и легким для разработки.
Поэтому главной целью курса будет показать, как создавать высоконагруженные приложения, какие инструменты помогут в этом, при чем при здесь тестирование, профайлинг, деплоймент и изначально правильно выбранная архитектура и откуда приходят основные ошибки.
На протяжении всего курса мы будем создавать веб-приложение, ориентированное на работу на ARM микро-компьютере Raspberry PI.
Требования к участникам: опыт в создании сайтов или проектов при помощи Python и популярных фреймворков. Учить создавать сайты с нуля не буду, буду помогать перейти на новый уровень и избегать довольно популярных и тем не менее назойливых ошибок.
Краткое содержания курса:
- Архитектура высоконагруженного приложения, разделение проекта на бэкенд и фронтэнд, удаленное выполнение задач
- Оптимизация архитектуры и оптимизация кода, что идет за чем
- Тестирование как двигатель разработки, а не наоборот
- Непрерывная интеграция и непрерывный деплоймент, сравнение мест для развертки проектов
- Откуда берутся основные ошибки веб-приложений, зачем мы наступаем на одни и те же грабли
Курс состоит из шести занятий. Лектор: Игорь Давыденко
Каждое занятие длится 2 часа с перерывом в 15 минут.
Стоимость занятия: 200 грн, стоимость полного курса: 1000 грн.
Оптимизация Python кода
Чтобы делать высокоэффективный код нужно уметь пользоваться профайлером, читать байткод, выполнять алгоритмическую оптимизацию и писать Python C Extensions если алгоритмически выжать уже ничего не получается.
Всем этим мы и займемся.
Требования к участникам: уметь программировать на Python и C. Последнее очень желательно хотя бы на уровне остаточных знаний из институтского курса — половина рассматриваемого кода будет на С.
Python 3.3, операционная система любая.
Краткое содержание курса:
- Профилирование через cProfile и timeit, анализ измеренных результатов. Рассматриваем из чего состоит функция с точки зрения Python и добираемся до байткода. Несколько простых вариантов оптимизации.
- Создаём простейший модуль Python C Extension.
- Учимся делать Python классы на C.
- Теперь пишем на Cython и радуемся как легко всё получается. В нагрузку ctypes.
- Показываю, как устроена виртуальная CPython машина изнутри. Интерпретаторы, потоки, стек. GIL. Как PyEval_EvalFrameEx исполняет байткод.
Курс состоит из пяти занятий. Лектор: Андрей Светлов.
Каждое занятие длится 2 часа.
Стоимость занятия: 300 грн.
UPD. Для тех кто ещё не понял: online версии не будет. Ни в каком виде.
среда, 17 апреля 2013 г.
Использование try-finally
Хочу обратить внимание на маленькую особенность написания конструкции try-finally.
Возьмём для примера многопоточность, а конкретно блокировки.
Где-то (наверное, в конструкторе класса) мы создали объект блокировки:
self.locker = threading.RLock()
Затем в каком-то методе мы пытаемся использовать эту блокировку в try-finally statement. Да, я знаю что RLock поддерживает context manager protocol и может использоваться в with statement. Так будет даже лучше, но мы сейчас говорим о другом варианте использования.
try:
self.locker.acquire()
do_some_work()
finally:
self.locker.release()
В чём ошибка? .acquire()
может выбросить исключение. Блокировка не
будет захвачена и попытка её освободить в .release()
выбросит новое
(другое) исключение. Что крайне нежелательно. Особенно в python 2.x,
где нет цепочек исключений. Т.е. ошибка в .acquire()
будет просто
скрыта, понять в чём было дело невозможно.
Правильно писать так:
self.locker.acquire()
try:
do_some_work()
finally:
self.locker.release()
Если было исключение в .acquire()
— то блокировка не захвачена и
освобождать её не нужно. Пусть обработка исключения разворачивается
своим ходом, .release()
в finally block совершенно не нужен.
Правило простое и понятное, тем не менее я сам нередко писал ошибочный код. А сегодня опять увидел это проблему при чтении чужих исходников.
Проблема усугубляется тем, что обычно .acquire()
работает успешно, и
лишь в редких случаях выбрасывает исключение. Которое мы видим в логах
(все используют логи, верно?) и недоумеваем, что именно произошло.
Это замечание относится к любому коду, выполняемому в finally block.
Переменные, блокировки, захват ресурсов, открытие файлов и т.д. должны быть выполнены перед try.
P.S.
На открытие файлов хочу обратить особое внимание как на самый частый случай. Куда более частый чем работа с многопоточностью. Правильно писать:
f = open('filename')
try:
f.read()
finally:
f.close()
Надеюсь, последний пример запомнится хорошо и внесёт ясность в головы уважаемых молодых коллег.
вторник, 16 апреля 2013 г.
DevConf 2013 ищет докладчиков
Московская конференция DevConf, которая состоится 14 июня, ищет докладчиков.
Сейчас в разделе Python всего две заявки: я и Lennart Regebro (классный дядька, с удовольствием его увижу снова и послушаю).
Неужели никто больше не желает ничего рассказать?
Темы исчерпались или порох отсырел?
P.S. Информация для иногородних: организаторы конференции берут на себя расходы по трасферу и проживанию. От вас же требуются хорошие интересные доклады.
четверг, 11 апреля 2013 г.
Pythons Innards
Это — серия из нескольких статей о том, как устроен CPython изнутри.
- Интерпретатор байткода
- Что из себя представляет этот самый байткод
- Что такое стек в понятии CPython, и как оно работает
- Как устроены дескрипторы, слоты и классы на нижнем уровне
- Пространства имен (namespaces)
Для «простых программистов, работающих работу» — наверное, ничего интересного.
Тем, кто желает разобраться в Питоне «до последнего байта» — очень рекомендую.
Вдобавок очень хочу посоветовать Ely Bendersky с его Python Internals. Эта серия статей тоже посвящена «внутреннему устройству» и прекрасно сочетается с тестами Yaniv Aknin.
- Как создаётся объект (в деталях)
- Что такое class и чем отличается от type
- Как именно происходит вызов callable
- И т.д. (symbol tables, например)
На самом деле подобного рода информации очень немного. Документация хорошо описывает CPython C API но не рассказывает о деталях, о том как это всё работает.
У меня до C кода дело доходило разве до обсуждения реализации GIL насколько я помню.
Если есть еще интересные статьи по внутреннему устройству — пишите в комментариях, я добавлю сюда. Наверняка что-то запамятовал, но в целом тема раскрыта очень скудно.
Так что если хотите узнать «как оно работает на самом деле» — читайте статьи по ссылкам.
UPD.
По устройству типов данных:
Строки
Целые числа
Списки
Словари (для версий Python <3.3)
Лекция Larry Hastings (release manager для Python 3.4, между прочим) с US PyCon 2012 на рассматриваемую тему.
понедельник, 4 марта 2013 г.
Python Sprint во Львове
Как я уже писал, 6 апреля будет Kyiv.py #10, на который уже зарегистрировались два докладчика: Ростислав Дзинько (Sockets from the ground up) и Тарас Ляпун (Cython - close to the metal python). Приходите, будет интересно.
А 7 апреля (да-да, на следующий день) во Львове пройдёт спринт по Питону, который организовывает Юля Савьюк @juljetta. Моя роль в этом мероприятии — sprint leader.
Регистрация, место и время
UPD:Форма регистрации: здесь.
Место проведения: офис Lohika, Лемківська 15, 3 этаж.
Начало в 11.00. Заканчиваем когда устанет последний боец. Реально, думаю, часов через шесть.
Что такое sprint?
Инструкция
Чтобы спринт был удачным, к нему нужно подготовиться.Приходим со своими ноутбуками (думаю, это не проблема).
На ноуте может быть любая система: Linux, Windows, Mac OS X.
Нужно установить компилятор С
- gcc на Линуксе идет в комплекте
- на Маке не знаю (там вроде бы можно еще и clang использовать, Питон должен его понимать)
- для Windows нужно установить Visual Studio, Visual Studio Express доступна для бесплатного скачивания и использования. Для работы нужны обе версии 2008 и 2010.
Также требуется Mercurial HG: http://mercurial.selenic.com/
Работа ведется над CPython repo, установленный из коробки Питон не подходит.
Подробные инструкции на английском языке здесь: http://docs.python.org/devguide/
Делаем:
$ hg clone http://hg.python.org/cpython
$ cd cpython
$ ./configure --with-pydebug
$ make -j4
make install делать не нужно
Далее. Чтобы не тратить время зря советую зарегистрироваться на bugs.python.org и просмотреть заранее список issues.
Выбирайте помеченные как easy — они как правило действительно простые.
Если у вас уже есть темы, над которыми хотите поработать — добро пожаловать.
Если не сможете определиться — тоже не беда, я подскажу.
Работа выглядит так:
- создается issue или берется готовая
- готовятся исправления
- делается патч: $ hg diff > issueXXX.diff
- он заливается через форму на bugs.python.org Там же добавляете меня (asvetlov) в nosy list
- я закачиваю патч и применяю его: $ curl http://path-to-patch|patch -p1
- если всё нормально — делаю push в repo
У Питона сейчас открыты для патчей четыре ветки: 2.7, 3.2, 3.3, default (3.4).
Новые фичи принимаются в default, баги и правки по документации могут попасть и в остальные три.
Если кто не знает как работать с ветками — я покажу на месте.
Юниттесты запускаются так:
$ make test
или
$ ./python Lib/test/regrtest.py
Отдельный тест можно запустить
$ ./python Lib/test/test_xxx.py
Перед отсылкой патча настоятельно рекомендую запускать чекер
$ make patchcheck
Эта процедура как минимум убирает лишние пробелы — постоянная головная боль при приеме патчей.