Архив рубрики: Python

Запуск процессов в tulip

В последнее время я работаю над запуском процессов в tulip: PEP 3156 и реализация на гуглокоде

Состояние дел на сегодня

Базовые конструкции выглядят так:

Транспорт:

class BaseTransport:
"""Base ABC for transports."""

def get_extra_info(self, name, default=None):
"""Get optional transport information."""

def close(self):
"""Closes the transport.

Buffered data will be flushed asynchronously. No more data
will be received. After all buffered data is flushed, the
protocol's connection_lost() method will (eventually) called
with None as its argument.
"""

class SubprocessTransport(BaseTransport):

def get_pid(self):
"""Get subprocess id."""

def get_returncode(self):
"""Get subprocess returncode.

See also
http://docs.python.org/3/library/subprocess#subprocess.Popen.returncode
"""

def get_pipe_transport(self, fd):
"""Get transport for pipe with number fd."""

def send_signal(self, signal):
"""Send signal to subprocess.

See also:
http://docs.python.org/3/library/subprocess#subprocess.Popen.send_signal
"""

def terminate(self):
"""Stop the subprocess.

Alias for close() method.

On Posix OSs the method sends SIGTERM to the subprocess.
On Windows the Win32 API function TerminateProcess()
is called to stop the subprocess.

See also:
http://docs.python.org/3/library/subprocess#subprocess.Popen.terminate
"""

def kill(self):
"""Kill the subprocess.

On Posix OSs the function sends SIGKILL to the subprocess.
On Windows kill() is an alias for terminate().

See also:
http://docs.python.org/3/library/subprocess#subprocess.Popen.kill
"""

Протокол:

class BaseProtocol:
"""ABC for base protocol class.

Usually user implements protocols that derived from BaseProtocol
like Protocol or ProcessProtocol.

The only case when BaseProtocol should be implemented directly is
write-only transport like write pipe
"""

def connection_made(self, transport):
"""Called when a connection is made.

The argument is the transport representing the pipe connection.
To receive data, wait for data_received() calls.
When the connection is closed, connection_lost() is called.
"""

def connection_lost(self, exc):
"""Called when the connection is lost or closed.

The argument is an exception object or None (the latter
meaning a regular EOF is received or the connection was
aborted or closed).
"""

class SubprocessProtocol(BaseProtocol):
"""ABC representing a protocol for subprocess calls."""

def pipe_data_received(self, fd, data):
"""Called when subprocess write a data into stdout/stderr pipes.

fd is int file dascriptor.
data is bytes object.
"""

def pipe_connection_lost(self, fd, exc):
"""Called when a file descriptor associated with the child process is
closed.

fd is the int file descriptor that was closed.
"""

def process_exited(self):
"""Called when subprocess has exited.
"""

Нужные методы в event loop:

class AbstractEventLoop:
"""Abstract event loop."""

def subprocess_shell(self, protocol_factory, cmd, *, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
**kwargs):
"""Run cmd in shell"""

def subprocess_exec(self, protocol_factory, *args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
**kwargs):
"""Subprocess *args"""

Т.е. интерфейс запуска процесса почти повторяет subprocess.Popen за исключением того, что subprocess.PIPE теперь вариант по умолчaнию. Заодно еще избавляемся от кошмара с правильным использованием shell=True (см. пост на эту тему). Поддерживаются только байтовые потоки, как и везде в tulip.

Оно уже в целом работает на Unix, код для Windows тоже готовится.

Делает всё что можно и нужно за исключением TTY. C TTY ван Россум предложил пока не связываться, да и subprocess его не поддерживает.

Проблема

Рабочие транспорты и протоколы — это, конечно, классно. Вполне подходящий низкоуровневый строительный блок.

Но простому программисту хочется иметь что-то более удобное и привычное.

Для tulip это должен быть код на основе yield from.

Проблема в том, что для процессов мы имеем не один поток ввода-вывода, а три однонаправленных: stdin, stdout и stderr. А еще процесс может сам решить закрыться, и это тоже нужно удобно обрабатывать.

Просьба

Я пытался придумать что-то такое, но результат пока мне не нравится.

Может, кто сумеет посоветовать дельную конструкцию? Или указать на готовую библиотеку, у которой можно поучиться?

Автор: Andrew Svetlov

Удаление объектов сборщиком мусора

На Хабре появилась толковая статьяо неприятностях, которые может доставить 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
...

Выводы

В результате всё просто. Используем слабые ссылки. Если это по каким-то причинам невозможно — поднимаем пороги.

Но при этом нужно помнить, что сборщик мусора будет запускаться реже.

Установка порогов в слишком большое значение способно в нашем случае съесть память не менее успешно, чем если бы эти значения оставались установленными по умолчанию.

Автор: Andrew Svetlov

Начало курсов «Learn Python»

Занятия начинаются в эту субботу, 29 июня 2013.

Записавшимся должно прийти на почту письмо с местом, временем и прочими деталями.

Автор: Andrew Svetlov

Скачивание файла с Google Docs и преобразование ods в csv

Возникла передо мной такая задача: мы храним таблицу соответсвия мака, ip и имени ПК в гуглодоксе, она и поддерживается в актуальном состоянии. DHCP сервер работает на FreeBSD, соответственно файл с настройками сервера вполне себе текстовый. И чтобы не обновлять данные и там и там был сделан скриптик. Наверняка можно было бы сделать проще, в некоторых случаях даже понятно как (например в функции csv2dhcp), но для небольшой таблицы я заморачиваться не стал.

Для работы понадобится pyquery и второй питон (считаем, что это у Вас уже есть). Кроме того, нам нужна библиотека для доступа к Google API.
Читать

Запуск MoinMoin2.0 под Apache22 на FreeBSD9

Предполагается, что moin2 расположен в /home/ishayahu/moin-2.0
Для начала нам надо установить сам апач и mod_wsgi, чтобы он мог работать с Flask (я использую для этого portmaster (/usr/ports/ports-mgmt/portmaster)):
#portmaster www/apache22 www/mod_wsgi
Далее создаём файл moin-2.0/moinmoin2.wsgi, чтобы апач мог запускать Flask приложение:
#для работы mod_wsgi, так как он блокирует sys.stdout
import sys
sys.stdout=sys.stderr
# Собственно для Flask
from MoinMoin.app import create_app
application = create_app('/home/ishayahu/moin-2.0/wikiconfig.py')

Теперь будем настроивать апач. Создадим конфигурацию виртульного хоста: файл /usr/local/etc/apache22/Includes/wiki.local.conf

    ServerAdmin meoc-it@mail.ru
    DocumentRoot /home/ishayahu/moin-2.0
    ServerName wiki.local
    ServerAlias www.wiki.local
    ErrorLog /home/ishayahu/wiki.local-error_log
    CustomLog /home/ishayahu/wiki.local-access_log combined
    WSGIDaemonProcess moinmoin2 user=ishayahu group=ishayahu threads=5
    WSGIScriptAlias / /home/ishayahu/moin-2.0/moinmoin2.wsgi
   
        WSGIProcessGroup moinmoin2
        WSGIApplicationGroup %{GLOBAL}
        Order deny,allow
        Allow from all
   
Настроиваем запуск вики как сервиса. Создаём файл для запуска апача в виртуальном окружении (нужно виртуальное окнужение для работы moin; не забыть chmod +x /root/start_wiki) /root/start_wiki:
#!/bin/bash
source /home/ishayahu/moin-2.0/env/bin/activate
/usr/local/etc/rc.d/apache22 onestart
Создаём файл для регистрации вики как сервиса /etc/rc.d/wiki (не забыть chmod +x /etc/rc.d/wiki):
#!/bin/sh
#
# PROVIDE: wiki
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name=»wiki»
start_cmd=»${name}_start»
stop_cmd=»/usr/local/etc/rc.d/apache22 stop»

wiki_start()
{
    /bin/bash /root/start_wiki
}

load_rc_config $name
run_rc_command «$1»
И в /etc/rc.conf:
wiki_enable=»YES»

Автор: Ishayahu Lastov

MoinMoin2. Документация. Введение в настройку MoinMoin

Типы конфигурационных файлов

Для изменения того, как moinmoin себя ведёт и выглядит, Вы можете настроить его, редактируя конфигурационные файлы:
  • Конфигурация движка Wiki
    • файл чаще всего называется wikiconfig.py, но может иметь и другое имя
    • в этом файле находятся классы конфига движка вики
    • он написан на Python
  • Конфигурация фреймворка
    • расположена в том же файле, что и конфигурация движка вики
    • содержит некоторые настройки в ВЕРХНЕМ РЕГИСТРЕ в конце файла. Это конфигурация фреймворка (Flask и его расширений)
    • он написан на Python
  • Конфигурация логирования
    • опциональна; если Вы не хотите настраивать логирование — будут использованы встроенные настройки
    • это отдельный файл, назвается чаще всего logging.conf
    • имеет .ini подобный формат файла

Меняйте понемногу и храните резервные копии

Начните с простого конфига, поставляемого с moin и меняйте его понемногу и каждый раз проверяйте работоспособность.
Если Вы не знакомы с форматом файла конфигурации, храните копию последнего работающего конфига на случай, если Вы вдруг сделаете трудно отлавливаемую ошибку.

Редактирование файлов Python

Когда Вы редактируете файлы Python, будьте осторожны с отступами, используйте лишь отступы, кратные 4 пробелам и не используйте табуляцию.
Кроме того, будьте внимательны к синтаксису вообще; это должен быть корректный код на Python, иначе Вы получите ошибку при попытке загрузки конфигурации. В таком случае прочитайте внимательно сообщение об ошибке, в нём скорее всего будет содержаться номер строки и описание проблемы. Если у Вас не получается решить проблему — восстановите из резервной копии последний рабочий конфиг.

Зачем использовать Python для конфигурации?

Вы, конечно, можете удивиться, почему мы используем код Python для конфигурации. Одна из причин в том, что это очень мощный язык. Moinmoin сам по себе разработан на Python и использование чего-либо другого потребовало бы больше времени для введения новой функциональности.

wikiconfig.py

wikiconf.py выглядит таким образом:
# -*- coding: utf-8 -*-
from MoinMoin.config.default import DefaultConfig

class Config(DefaultConfig):
    # некоторый комментарий
    sometext = u'your value'
    somelist = [1, 2, 3]

MOINCFG = Config # Flask любит только верхний регистр
SOMETHING_FLASKY = 'foobar'
Давайте разберём это строка за строкой:
  1.  Мы объявляем кодировку конфигурационного файла; убедитесь, что ваш редактор использует ту же самую кодировку, особенно, если Вы используете не ASCII символы
  2. импортируем класс DefaultConfig из кода moin; в нём содержатся значения по умолчанию для всех настроек, что экономит ваш труд, так как Вы должны указать тут только те параметры, которые Вы хотите переопределить
  3. пустая строка для лучшего восприятия
  4. определяем новый класс Config, дочерний классу DefaultConfig. Это и есть настройки движка вики и он переопределяет значения из DefaultConfig.
  5. знак # указывает на комментарий в вашем конфиге. Эта строка, как и последующие строки в пределах Config имет отступ в 4 пробела, так как Python определяет блоки по отступам
  6. определяем атрибует Config'a c именем sometext и значением u'your value', где «u» указывает на то, что это строка юникода
  7. определяем атрибует Config'a c именем somelist и значением [1, 2, 3], это список с эементами 1, 2 и 3
  8. пустая строка для лучшего восприятия
  9. специальная строка «MOINCFG = Config» должна иметь именно такой вид по техническим причинам
  10. код в верхнем регистре находится в конце файла, вне класса Config и является настройкой фреймворка; обычно это что-то для Flask'a или его расшире