понедельник, 20 октября 2014 г.

Почему я не люблю Flask

Есть такой популярный microframework: Flask.

Многим нравится: легкий и простой для изучения, то да сё.

А мне --- категорически нет.

Нелюбовь началась с элементарного: request --- это thread local variable:

import flask
from myapp import app

@app.route('/')
def handler():
    req = flask.request
    if 'arg' in req.args:
        process_arg(req.args['arg'])
    ###

Т.е. для для того чтобы узнать с какими GET или POST параметрами вызвали мой код -- я должен обращаться к глобальной переменной!

Я знаю разницу между global variable и thread local variable если что -- но это не избавляет от неприятного послевкусия.

Ага, есть еще и flask.g!

Если уж мне потребуются context local variables -- я их буду использовать по моему выбору, морщась от осознания собственного несовершенства. Зачем flask их мне навязывает?

Дальше -- больше.

Смотрим еще раз:

from myapp import app

@app.route('/')
def handler():
    ###

Имеем наполовину сконфигурированный импортированный откуда-то app, к которому добавляем обработчик.

Мне это не нравится. Я хочу сделать app и добавить в него route table.

Flask это позволяет, но документация провоцирует делать ровно наоборот.

Исполнять код на этапе импорта модуля не выглядит хорошей идеей, сейчас в этом я полностью уверен.

Идем дальше.

Параметры в route:

@app.route('/user/<username>')
def handler(username):
    pass

Весной это казалось мне удачным. Даже сделал что-то похожее в aiorest.

Потом понял, что штука абсолютно бесполезная: нам всегда требовалось что-то из HTTP HEADERS, COOKIES и GET/POST parameres в обработчике запроста.

Чтобы проверить -- авторизирован ли пользователь, например.

Выпилил.

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

route args, GET, POST, COOKIES -- каждый dict может иметь перекрывающиеся имена-названия.

Паша Коломиец в zorro попытался решить проблему через аннотации:

def handler(self, request: Request):
    pass

Т.е. handler имеет параметр с аннотацией Request -- он получит в него request object.

В zorro можно регистрировать свои аннотации для получения дополнительной информации.

Симпатично и элегантно -- но слишком сложно для библиотеки для чайников.

Это путь настоящих джедаев -- я же в последние годы пропагандирую применять метапрограммирование как можно реже: когда без трюка совсем не обойтись и его применение настолько простое и очевидное, что ошибиться просто невозможно.

Заключение

Я не призываю не использовать flask, у меня нет такой цели. Хотите граблей -- получайте.

Просто сейчас я занялся добавлением в aiohttp WEB-сервера, пригодного для использования простым программистом.

И я точно знаю, чего не будет в aiohttp -- контекстных переменных и зависимостей на этапе импорта.

aiohttp.web должен быть прост насколько это возможно, но не проще.

Желающие выстрелить себе в ногу пусть делают это в библиотеках, построенных на основе aiohttp.web -- мы дадим им такую возможность.

Базис должен быть простым и дуракоустойчивым -- даже если для этого придётся написать несколько лишних строк кода.

понедельник, 6 октября 2014 г.

pep8 и 80 символов в строке

На самом деле 79 если внимательно читать pep 8, если что...

Посетил на днях Python Party, организованной компанией Yandex.
Мероприятие понравилось, а самый интерес был потом, на "поболталках" в кабаке.

Был на party доклад от Кирилла Борисова "Контроль за стилем кода". Интересный и толковый.

Только я возражаю против с высказывания вроде:
  -- pep8 рекомендует ограничивать длину строк в 79 символов максимум. Мы с этим несогласны -- сейчас мониторы большие, можно писать 120 символов и это великолепно помещается на экране.

Я везде строго пишу с ограничением на 79 символов.

Попробую объяснить почему.

1. Во первых сам код Python так написан и patch вылезающий за границы просто не будет принят. OK, я committer -- значит тем более обязан следовать соглашениям.
2. Во вторых мой редактор (emacs если что) настроен на то чтобы подсвечивать длинные строки. И когда я открываю код библиотеки, наплевавшей на ограничение по длине строки -- у меня половина экрана "красная". Это огорчает.
3. В третьих и главное: если у вас широкий монитор -- это прекрасная возможность разбить его по вертикали и видеть одновременно несколько редактируемых файлов. У меня даже на 14'' ноутбуке Full HD -- это значит что при размере шрифта в 13pt у меня помещается два буфера. Коллега на 24'' привык работать в vim с шестью буферами: 3х2. Это очень удобно -- гораздо лучше видеть сразу несколько файлов чем один, но с длинными строками.

Что до "невозможности" уместить код в 79 символов -- это распространенное заблуждение.
При некотором навыке всё легко получается.

К тому же такой подход провоцирует сохранение промежуточных вычислений в локальные переменные -- что хорошо само по себе, так как улучшает читабельность кода (вы же даете переменным не слишком длинные, но "говорящие" имена, верно?)

Коротко говоря, 79 символов заставляют лучше писать код и помогают его читать. Что вам всем и рекомендую.

пятница, 5 сентября 2014 г.

Абстрактные классы для коллекций

Пусть мы сделали какой-то класс для хранения набора данных. Например, настроек вида ключ -> значение:
class Settings:

    def __init__(self):
        self._data = {}

    def add_property(self, key, value):
        assert isinstance(key, str), key
        self._data[key] = value

    def get_property(self, key):
        assert isinstance(key, str), key
        return self._data[key]
И тут нам в голову приходит удачная идея, что было бы здорово вместо вызова settings.get_property('key') использовать квадратные скобки как для dict: settings['key']:
def __getitem__(self, key):
    return self.get_property(key)
Что не так?
То, что наш класс стал отчасти походить на readonly dict (он же mapping) -- но он не реализует весь предполагаемый контракт.
Так, я привык, что если класс похож на readonly dict, то он позволяет узнать количество элементов в нём. Добавляем __len__:
def __len__(self):
    return len(self._data)
Всё ещё не хорошо. Для mapping обычно можно итерироваться по ключам. Добавление __iter__ решает проблему:
def __iter__(self):
    return iter(self._data)
Всё? Нет! Хочется ещё проверять на наличие ключа: key in settings -- dict ведь это позволяет!
Можем добавить метод __contains__ -- а можем вспомнить, что есть класс collections.abc.Mapping.
Это абстрактный базовый класс, задающий контракт для неизменяемого словаря.
Описание того, что таке абстрактный базовый класс -- здесь
Просто наследуемся от Mapping:
from collections.abc import Mapping

class Settings(Mapping):

    # ...
В качестве бесплатного бонуса получам поддержку .get(), .keys(), .items(), .values(), __eq__ и __ne__.
Реализация этих методов не оптимальная с точки зрения производительности, но она уже есть из коробки. К тому же всегда можно добавть свой вариант, который будет работать быстрее стандартного (если мы знаем как это сделать).
Если мы забудем реализовать какой-то критически важный метод -- при создании экземпляра класса получим исключение:
>>> settings = Settings()
TypeError: Can't instantiate abstract class Settings with abstract methods __iter__
В стандартной библиотеке есть большой набор абстрактных базовых классов:
  • ByteString
  • Callable
  • Container
  • Hashable
  • ItemsView
  • Iterable
  • Iterator
  • KeysView
  • Mapping
  • MappingView
  • MutableMapping
  • MutableSequence
  • MutableSet
  • Sequence
  • Set
  • Sized
  • ValuesView
Очень рекомендую изучить набор методов, реализуемых этими классами -- помогает понять систему типов собственно Питона.
При необходимаости можно (и нужно) написать свои.
А в заключение забавный пример.
В библиотеке sqlalchemy есть класс RowProxy для строки-кортежа, получаемой в результате SQL запроса.
Класс выглядит как mapping: имеет длину, .keys(), .items(), .__contains__() и все прочие нужные методы. Позволяет получать значение как по позиционному номеру так и по названию колонки в базе данных.
При этом он реализует контракт Sequence (как у tuple).
Т.е. iter(row) возвращает данные, а не названия колонок. И это немного сбивает с толку: выглядит как утка, а крякает как поросёнок.
В оправдание sqlalchemy могу сказать, что RowProxy появился в самой первой версии алхимии, еще до того как в Питон добавили collections.abc. А потом что-то менять стало поздно.
Но сейчас при разработке собственных библиотек стоит придерживаться устоявшихся стандартов и активно применять абстрактные базовые классы для коллекций.

Перегрузка операций

В питоне буквально все используют магические методы. Когда пишем конструктор класса -- называем его __init__ и т.д.

Надеюсь, все умеют писать такие вещи, у меня нет желания останавливаться на основах подробней.

Поговорим о правильной перегрузке математических операций.

Создаем класс-точку

Итак, имеем точку в двухмерном пространстве:

class Point(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'Point({}, {})'.format(self.x, self.y)

В этом варианте сразу бросается в глаза недочет: точка позволяет изменять свои координаты. Это плохо, ведь точка с другими координатами -- это уже другая точка. И число int, в отличие от списка, не позволяет изменять себя -- только создавать новые в результате арифметических операций.

Обновленная версия:

class Point(object):

    def __init__(self, x, y):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self._x

    @property
    def y(self):
        return self._y

Как взрослые люди мы используем джентельменское соглашение, по которому внешний пользователь класса не должен обращаться к именам, начинающимся с подчеркивания.

Над точками нужно производить какие-то операции. Самая, наверное, распространенная -- это сравнение.

Сравнение

class Point:

    # ...

    def __eq__(self, other):
        return self._x == other._x and self._y == other._y

Что плохо? То, что попытка сравнить точку с не-точкой (Point(1, 2) == 1) выбросит исключение AttributeError:

>>> Point(1, 2) == 1
AttributeError: 'int' object has no attribute '_x'

в то время как стандартные питоновские типы ведут себя иначе:

>>> 1 == 'a'
False

Меняем сравнение:

def __eq__(self, other):
    if not isinstance(other, Point):
        return False
    return self._x == other._x and self._y == other._y

Теперь сравнивание работает почти правильно:

>>> Point(1, 2) == Point(1, 2)
True

>>> Point(1, 2) == 1
False

Слово почти я употребил потому, что Питон работает так:

  • сначала пытается сделать сравнение a == b
  • если сравнение не дает результата -- делается вторая попытка с перестановкой операторов b == a

Чтобы сказать, что операция сравнения не дает результата -- нужно вернуть константу NotImplemented (не путать с исключением NotImplementedError):

def __eq__(self, other):
    if not isinstance(other, Point):
        return NotImplemented
    return self._x == other._x and self._y == other._y

В паре с == всегда идет оператор !=, не нужно про него забывать:

def __ne__(self, other):
    return not (self == other)

На самом деле Питон будет сам использовать метод __eq__ если __ne__ не определен, но я считаю что лучше и понятней написать __ne__ самому, тем более что это не трудно.

hash

Наши точки сравниваются, всё прекрасно. Но если мы захотим, скажем, использовать их как ключи в словаре -- получим ошибку:

>>> {Point(1, 2): 0}
TypeError: unhashable type: 'Point'

Нужно определить метод __hash__. Питон прекрасно умеет считать хэш для кортежа, чем мы и воспользуемся:

def __hash__(self):
    return hash((self._x, self._y))

Результат:

>>> {Point(1, 2): 0}
{Point(1, 2): 0}

Определять только __hash__ без __eq__/__ne__ неправильно: в случае коллизии задействуются операторы сравнения. Если они не определены -- можно получить некорректный результат.

Упорядочивание

Как говорил один преподаватель, не используйте слово "сортировка" -- оно очень созвучно слову "сортир".

Точки на плоскости не имеют естественного порядка. Поэтому реализовывать операторы упорядочивания (<, >, <=, >=) не нужно.

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

Если для какой-то цели вы придумали принцип упорядочивания для точек на плоскости -- сделайте это нормальным методом класса со своим именем, не нужно вводить в изумление пользователей.

Арифметика

Точки можно складывать и вычитать.

def __add__(self, other):
    if not isinstance(other, Point):
        return NotImplemented
    return Point(self._x + other._x, self._y + other._y)

def __sub__(self, other):
    if not isinstance(other, Point):
        return NotImplemented
    return Point(self._x - other._x, self._y - other._y)

Пример:

>>> Point(1, 2) + Point(2, 3)
Point(3, 5)

>>> Point(1, 2) - Point(2, 3)
Point(-1, -1)

Так как точки неизменяемые, то возвращается новый объект.

Вообще оператор + подразумевает, что мы создаем в качестве результата что-то новое, а не меняем какой-то из аргументов.

Как и для сравнения, если не знаем что делать -- возвращаем NotImplemented. Тогда Питон попробует переставить аргументы местами, но вызовет уже __radd__:

res = a.__add__(b)
if res is NotImplemented:
    res = b.__radd__(a)

Реализуем и эти методы:

def __radd__(self, other):
    if not isinstance(other, Point):
        return NotImplemented
    return other + self

def __rsub__(self, other):
    if not isinstance(other, Point):
        return NotImplemented
    return other - self

Зачем это нужно? Допустим, мы хотим складывать наши точки с QPoint из библиотеки PyQt, полуая в результате опять объекты класса Point.

Тогда нужно расширить наши __add__ и __radd__:

def __add__(self, other):
    if isinstance(other, Point):
        return Point(self._x + other._x, self._y + other._y)
    elif isinstance(other, QPoint):
        return Point(self._x + other.x(), self._y + other.y())
    return NotImplemented

def __radd__(self, other):
    if isinstance(other, Point):
        return Point(self._x + other._x, self._y + other._y)
    elif isinstance(other, QPoint):
        return Point(self._x + other.x(), self._y + other.y())
    return NotImplemented

Реализацию __iadd__/__isub__ рассматривать не буду, там всё очевидно. К тому же Питон сам способен сделать как надо, сконструировав нужный код на основе вызовов __add__/__sub__.

Умножение и деление для точек не имеют смысла, поэтому их просто не делаем. Если бы делали, скажем, операции над векторами -- ввели бы скалярное и векторное произведения. "Просто точкам" эти излишества не нужны.

Заключение

Вот и всё, набросок для класса точки готов.

Надеюсь, хотя бы некоторым читателям написанное окажется полезным.

понедельник, 1 сентября 2014 г.

Вакансии в Levelup

Level Up, где я работаю, снова набирает питощиков.

Уровень специалистов этого набора -- от среднего и выше.

Работать предстоит над созданием внутренних продуктов (да-да, мы -- продуктовая компания).

В основном это REST сервисы с некоторым количеством Web интерфеса.

Новые продукты делаем на Python 3, асинхронное сетевое программирование на asyncio -- так что не скучаем.

Платят деньгами.
Работа в офисе на Подоле (Киев).
Традиционный полный рабочий день, зефир в холодильнике и абонементы на спортзал/бассен, всё такое прочее.
Если вакансия заинтересовала — пишите на andrey.svetlov@levelupers.com

пятница, 27 июня 2014 г.

aiohttp client для Sentry

Полагаю, многие используют Sentry для мониторинга ошибок в своих проектах.
Мне тоже захотелось.
Sentry для Python предоставляет библиотеку под названием raven чтобы ваш код мог слать сообщения на Sentry server.

Была проблема: код наших рабочих проектов использует asyncio и основанные на этой штуке библиотеки.
Делать синхронные HTTP вызовы в асинхронном коде нехорошо -- это очень плохо влияет на производительность.

Сегодня нужный Pull Request вошел в raven-python master.
DSN для aiohttp транспорта должен быть вида:
aiohttp+http://10797b99a237420b874b7be422a715f8:4b5a65ea48bf43c0811ff473d0ae8563@sentry.project.com/2

Ждите следующего релиза raven-python (или пользуйтесь git master на свой страх и риск :)

пятница, 20 июня 2014 г.

aiohttp войдет в FreeBSD ports

Библиотека aiohttp предназначена для поддержки HTTP протокола в asyncio -- новом стандарте (PEP 3156: Asynchronous IO Support Rebooted: the "asyncio" Module) и эталонной реализации асихронного сетевого программирования для Python.

aiohttp состоит из клиентской (HTTP запросы) и серверной (HTTP сервер) частей.

HTTP клиент довольно хорош, умеет обрабатывать redirects, cookies, HTTP proxies и много чего еще.

Состояние дел с HTTP сервером немного хуже. Т.е. он вполне рабочий, умеет делать keep-alive, gzip compression, chunked encoding и web-sockets. Проблема в том что HTTP сервер слишком низкоуровневый, нужно написать немало кода чтобы отдать простой html response.
Мы работаем над более высокоуровневым API.

И, прошу прощения, документация отсутствует (мы тоже работаем над этой проблемой). Примеры использования есть, а документации пока нет.

Так вот, aiohttp уже добавлена в FreeBSD ports git repo (когда выйдет релиз новых портов -- не знаю).
Мелочь, а приятно.

К слову, в Gunicorn 19.0 появился новый gaiohttp worker который, как не трудно догадаться, написан на базе aiohttpпост на эту тему.

В общем, двигаемся не так быстро как нам хотелось бы -- но работа идет. И есть надежда, что к концу лета напишем документацию и предоставим хороший высокоуровневый Server API.


понедельник, 9 июня 2014 г.

asyncio, aiohttp и gunicorn

Многие WSGI сайты используют gunicorn для развертывания.

Это прекрасно работает для python 2, но для python 3 есть проблема: наиболее употребляемые gunicorn workers, -- использующие gevent и enentlet, -- не заводятся под "тройкой" потому что gevent c eventlet все еще не портированы.

Автор gunicorn Бенуа Шесно начал было писать новый asyncio worker -- но быстро увидел что работы предстоит сделать немало, а в aiohttp Николай Ким уже написал всё что нужно.

Коротко говоря aiohttp.worker переехал в gunicorn.workers.gaiohttp вместе со своими тестами, вышел новый релиз aiohttp 0.8, а мы с Колей стали commiters в gunicorn.

Бенуа планирует выпустить новый релиз gunicorn к концу недели.

К сожалению aiohttp 0.8 ломает обратную совместимость -- так было нужно.
Зато теперь он имеет более чистый API: использует multidict для HTTP headers в клиентском и серверном коде вместо причудливой смеси разных подходов, привносящих в public API методы, которые было небезопасно использовать.
Плюс переработана иерархия исключений и еще кое-какие приятные мелочи вроде более внятных сообщений об ошибках -- полный список изменений здесь.
Это еще один шаг к тому, чтобы сделать aiohttp максимально удобной для простого программиста.

К слову, при использовании gaiohttp gunicorn worker можно делать странные вещи: запускать асинхронно asyncio.Task из вьюхи на Django, например. Как это делают для gevent/eventlet worker.
Мне такой гермафродит не нравится по эстетическим соображениям, но запретить стрелять себе в ногу я не в силах. А на stack overflow подобные хотелки уже были.

среда, 4 июня 2014 г.

Итоги PyCon RU

Вернулся из Екатеринбурга с pycon.ru
Всё прошло замечательно.

  • Прекрасное место в сосновом лесу (воздух там пахнет восхитительно).
  • Хорошая организация мероприятия. Девочки из IT People уже второй раз показывают очень высокий класс -- накладок практически не было.
  • Пара интересных заманух: впервые увидел 3D принтер в работе и попробовал очки виртуальной реальности -- картинка гадостная, но действует на мозг мощно... Я понимаю что это только видео, но тело само пытается реагировать. Если бы не поддерживали -- упал бы точно. А так просто сильно вспотел, стоя на месте.
  • Доклады. Те, на которые попал -- послушал с интересом. Может, были и неудачные -- не знаю, не видел. Впрочем, видел далеко не всё.
  • Вечерний (или, точнее, ночной) костер был суперской идеей.
  • Немного досаждали комары -- но и те, проявляя деликатность, поедом не ели.
  • Хотелось бы посетить больше интересных мне докладов -- но они или пересекались с другими не менее интересными, или просто очень хотелось спать: ночной костер был прекрасен и уходить решительно не хотелось.
Очень удачно, что доклад Андрея Власовских был в расписании перед моим.
Андрею задали столько вопросов по asyncio (на которые он часто отвечал: завтра услышите от Светлова) -- что я был вынужден сильное скорректировать план своей лекции. Надеюсь, у меня вышло более или менее неплохо. Похоже, asyncio интересна довольно большой части питоновского сообщества -- что меня радует.


Самое главное -- получилось интересно поболтать ребятами и девчонками.
Каких-то собеседников увидел впервые.
Других -- знаю уже много лет, но увидеть вживую могу только примерно раз в год на конференциях вроде этой.
И это очень классно: не переписываться а просто поговорить!

Организаторам -- огромное спасибо. С радостью приеду на PyCon RU 2015.

Мои слайды:

P.S. Через неделю в Москве будет конференция DevConf.

Так получилось, что две конфы по Python почти совпали по времени, но оказались довольно сильно разнесены географически.

Часть докладов одни и те же.
Мне выпал второй шанс -- посещу то, что пропустил на PyCon RU: ребят из WarGaming, лекцию по Salt и Ansible, Романа Иманкулова о pytest.

Роман умееет очень интересно рассказывать о чем угодно. Я ортодоксально использую исключительно стандартный unittest и он меня не переубедит -- но послушаю с удовольствием..

Salt и Ansible тоже очень итересны мне лично.

И повторю я свои лекции. По asyncio будет немного другая -- с учетом тех вопросов, которые мне задавали слушатели после выступления.
"Рецепты по написанию хорошей библиотеки" не изменятся -- выстрадано, продуманно, подкреплено опытом.

И, к тому же, формат DevConf немного другой -- больше времени и меньше аудитория.
Поэтому я опять попробую форму изложения, лучше подходящую для DevConf: 
  • Я рассказываю без микрофона (зал достаточно мал)
  • Если возникает непонятка -- не ждите конца лекции а поднимите руку (правую или левую, мне без разницы), чтобы я заметил. 
  • После этого громко (микрофона нет) задайте вопрос -- я отвечу.
  • Доклад будет скомпонован так, чтобы показать самые важные вещи в начале, -- и даже если я не успею рассказать "хвост", -- ничего страшного не произойдёт. 
  • Поэтому -- не стесняйтесь задавать вопросы.
В общем, думаю, идею все поняли.


понедельник, 12 мая 2014 г.

LevelUp ищет питонщиков

Компания Level Up, где я имею честь работать, набирает питонщиков.

Уровень кандидатов может быть разный: мы ищем талантливых людей.

Особенно интересуют позиции senior software engineer и team/tech leader.

Задачки интересные.

Технологии разные:

  • стандартный WSGI (я предпочитаю Pyramid, много где используется Django)
  • асинхронное программирование на Tornado и Twisted
  • базы данных — самые разные: redis, PostgreSQL, MySQL/MariaDB, mongo. Осторожно пробуем Cassandra.
  • для новых разработок применяем Python 3 и asyncio
  • есть задачи для спецов по data mining и анализу текстов

Зоопарк технологий объясняется тем, что компания работает над кучей взаимосвязанных сервисов, которые общаются между собой через REST API. При этом сервисы могут быть написаны на разных платформах (Java, PHP, node.js) и тут уже протокол взаимодействия гораздо важнее конктретных библиотек.

Бизнес построен не на аутсорсинге.

Мы — продуктовая компания

Т.е. мы создаём инструменты, которые сами же используем. Это очень здорово и удобно, когда постановщик бизнес-требований сидит в соседней комнате.

И вместе с тем у нас нет ужасного монолитного кода, написанного во времена когда по Земле бродили мамонты, — и который должен поддерживаться во что бы то ни стало потому что тестов нет, а менять без тестов страшно.

Работа в офисе на Подоле (Киев).

Традиционный полный рабочий день, зефир в холодильнике и абонементы на спортзал/бассен, всё такое прочее.

Если вакансия заинтересовала — пишите на andrey.svetlov@levelupers.com

среда, 7 мая 2014 г.

aiopg и SQLAlchemy

Выпустил новую версию aiopg 0.2 -- библиотеки для работы с PostgreSQL из asyncio.

aiopg использует асинхронные вызовы и в этом похож на txpostgres и momoko -- библиотеки для работы с PostgreSQL под twisted и tornado соответственно.

В новой версии aiopg появилась опциональная поддержка SQLAlchemy Core Expressions.

Проще один раз показать.

Создаем описание структуры базы данных:

import sqlalchemy as sa

metadata = sa.MetaData()

users = sa.Table('users', metadata,
                 sa.Column('id', sa.Integer, primary_key=True),
                 sa.Column('name', sa.String(255)),
                 sa.Column('birthday', sa.DateTime))

emails = sa.Table('emails', metadata,
                  sa.Column('id', sa.Integer, primary_key=True),
                  sa.Column('user_id', None, sa.ForeignKey('users.id')),
                  sa.Column('email', sa.String(255), nullable=False),
                  sa.Column('private', sa.Boolean, nullable=False))

Как видите -- две таблицы, связанные отношением один-ко-многим. Для тех, кто не знаком -- алхимия позволяет описать любую модель данных, которая только может прийти в голову. Индексы, constraints, пользовательские типы данных такие как array и hstore -- тоже.

Теперь нужно сделать engine:

from aiopg.sa import create_engine

engine = yield from create_engine(user='aiopg',
                                  database='aiopg',
                                  host='127.0.0.1',
                                  password='passwd')

engine содержит внутри connection pool.

Для работы с БД нужно получить connection и что-нибудь выполнить:

with (yield from engine) as conn:
    uid = yield from conn.scalar(
        users.insert().values(name='Andrew', birthday=datetime(1978, 12, 9)))

Обратите внимание: диалект знает о INSERT ... RETURNING и позвращает primary key для вставляемой записи.

Работа с транзакциями:

with (yield from engine) as conn:
    tr = yield from conn.begin()

    # Do something

    yield from tr.commit()

Получение данных:

with (yield from engine) as conn:
    res = yield from conn.execute(users.select())
    for row in res:
        print(res)

Сложный запрос:

with (yield from engine) as conn:
    join = sa.join(emails, users, users.c.id == emails.c.user_id)
    query = (sa.select([users.c.name])
             .select_from(join)
             .where(emails.c.private == 0)
             .group_by(users.c.name)
             .having(sa.func.count(emails.c.private) > 0))

    print("Users with public emails:")
    ret = yield from conn.execute(query)
    for row in ret:
        print(row.name)

Вызов SQL функций:

with (yield from engine) as conn:
    query = (sa.select([sa.func.avg(sa.func.age(users.c.birthday))])
             .select_from(users))
    ave = (yield from conn.scalar(query))
    print("Average age of population is", ave,
          "or ~", int(ave.days / 365), "years")

sa.func.avg и sa.func.age выполняются на стороне SQL сервера.

Полный код примера здесь, документация здесь.

суббота, 3 мая 2014 г.

Работаем с pip

Думаю, все применяют pip и знают основы:

$ pip install -U sqlalchemy
$ pip install -r requirements.txt
$ pip freeze > requirements.txt
$ pip uninstall sqlalchemy

Давайте посмотрим, что ещё полезного умеет эта команда

Ставим пакет локально для пользователя

$ pip install --user pep8

Т.е. если мы не в виртуальном окружении (virtualenv/virtualenvwrapper), то пакет pep8 будет установлен куда-то вроде ~/.local/lib/python3.4/lib.

Главная прелесть метода -- не нужны права суперпользователя для установки пакетов (не надо писать sudo pip install ..., К тому же так гораздо аккуратней.

Создаем конфигурационный файл

Т.к. опция --user нужна часто, стоит записать её в конфиге как значение по умолчанию. Создаем файл ~/.pip/pip.conf и пишем в него

[install]
user = true

Готово.

Внимание: user=true не работает с виртуальными окружениями.

Принудительно переустанавливаем пакет

Реальный пример: я поставил pyzmq. Библиотека скачалась и скомпилировалась. Потом я заметил что она скомпилировалась со неправильной версией libzmq. libzmq я переставил, теперь нужно пересобрать pyzmq:

$ pip install -U --force-reinstall pyzmq

Для работы --force-reinstall обязательно нужно указывать -U (--upgrade).

Смотрим на список установленных (доступных) библиотек

$ pip list

Выясняем, какие библиотеки можно обновить

$ pip list -o
$ pip list --outstanding

Для выполнения команды pip неоднократно делает зазыр в интернет, так что придется подождать.

Устанавливаем библиотеку для работы над ней

Находясь в корне проекта, в той же папке где лежит setup.py:

$ pip install -e .

Этот способ лучше вызова

python setup.py develop

хотя бы тем что работает даже если setup.py не использует setuptools.

Установка необязательных зависимостей

Если setup.py содержит extras_require то дополнительные зависимости можно установить так:

$ pip install -e .[PDF,reST]

Получаем информацию об установленной библиотеке

$ pip show pyflakes

Так можно узнать версию, местонахождение на диске и зависимости.

Ищем в PyPI

msgpack имеет неудобное имя библиотеки (msgpack-python), которое я постоянно забываю.

$ pip search msgpack

покажет список всех пакетов, в названии которых есть msgpack.

Только не делайте:

$ pip search django

Дуплит пару минут, выдает 5754 результата :)

Автодополнение

И, наконец, последнее. Я люблю пользоваться bash completion. Чтобы настроить эту удобную штучку для pip выполните:

$ pip completion --bash >> ~/.bashrc

или для zsh:

$ pip completion --zsh >> ~/.zprofile

Как ни странно pip --help стесняется рассказать о том, что у него есть команда pip completion -- но оно работает.

понедельник, 28 апреля 2014 г.

DevConf 2014

14 июня будет ежегодная конференция разработчиков DevConf 2014.

Там есть и питоновская секция.

Посещал конфу последние четыре года, собираюсь и в этот раз.

Если хотите послушать доклады и пообщаться с коллегами -- покупайте билеты.

Желающим выступить -- заявки на доклады принимаются еще две недели.

понедельник, 14 апреля 2014 г.

aiozmq benchmark -- обновление

Вчера написал измеритель производительности aiozmq.

Аж пять тестов, три для pyzmq и два для aiozmq.

Сегодня захотел посмотреть, насколько правильные цифры они показывают.

Для оценки результатов измерений давным-давно была изобретена математическая статистика. Имея выборку (результаты нескольких замеров) вычисляем среднеквадратическое отклонение и доверительный 95% интервал. Последний более понятен для интерпретации: если на пальцах то это тот диапазон, в который попадает наша величина с вероятностью 95%.

Дело в том что результаты измерений всегда дают некоторый разброс, и нужно оценить насколько этот разброс велик (можно ли верить полученному среднему значению).

В numpy / scipy уже всё есть, математику вспоминать не нужно.

Пример для вычисления request per second для списка data, каждый элемент которого содержит время выполнения count запросов RPC.

from scipy.stats import norm, tmean, tvar
from numpy import array

rps = count / array(data)
rps_mean = tmean(rps)
rps_var = tvar(rps)
low, high = norm.interval(0.95, loc=rps_mean, scale=rps_var**0.5)

Анализ

И что я увидел? Тесты для aiozmq давали просто чудовищный разброс: среднеквадратическое отклонение зашкаливало, а нижняя граница доверительного интервала иногда уходила в минуса.

Т.е. моим тестам просто нельзя верить!!!

Несколько раз перепроверив убедился, что проблема именно в тестах а не в неправильной обработке статистики.

Начал рабираться и довольно быстро обнаружил источник беды: я не освобождал все ресурсы после запуска теста (не закрывал транспорты и сервисы aiozmq). В результате при каждом следующем запуске тестового кода asyncio event loop был вынужден делать чуть больше работы. Совсем чуть-чуть, но ведь мы измеряем десятые доли милисекунды и разница начинает быть заметной.

Быстро поправил и жизнь наладилась.

Плюс Олег Нечаев заметил, что тест для aiozmq core не вполне корректен и занимает на 25% больше времени чем должен.

Первоначальные 3-5 запусков каждого теста были явно недостаточны для сбора статистики. Из институтских лаб помню, что выборка в которой меньше 30 измерений -- и не выборка уже а сплошное недоразумение. Увеличил количество до 100.

Чтобы тесты показывали более стабильные результаты увеличил до 5000 количество request-reply для каждого теста.

Время работы измерялки стало напрягать. Добавил запуск тестов на нескольких процессах.

У меня четыре виртуальных ядра, так что время уменьшилось в ожидаемые 4 раза.

Обновленные тесты:

(aiozmq)andrew@tiktaalik-3:~/projects/aiozmq (master)$ python benchmarks/simple.py -n 5000 -t 100
Run tests for 100*5000 iterations: ['aiozmq.rpc', 'core aiozmq', 'single thread raw zmq', 'single thread zmq with poller', 'zmq with threads']
....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

RPS calculated as 95% confidence interval
Results for aiozmq.rpc
RPS: 3797: [3589, 4005],    mean: 263.533 μs,   standard deviation 7.252 μs

Results for core aiozmq
RPS: 10875: [9806, 11944],  mean: 92.190 μs,    standard deviation 4.842 μs

Results for single thread raw zmq
RPS: 20622: [14559, 26684], mean: 49.840 μs,    standard deviation 9.238 μs

Results for single thread zmq with poller
RPS: 18020: [16687, 19353], mean: 55.572 μs,    standard deviation 2.147 μs

Results for zmq with threads
RPS: 19550: [14768, 24331], mean: 52.101 μs,    standard deviation 7.761 μs

Машинка другая, десктопный Intel(R) Core(TM) i5-3470 CPU @ 3.20GHz, 8GB, 2(4) cores(hyperthreads).

Так что абсолютные результаты поменялись относительно прошлого раза, при этом их соотношение осталось примерно тем же. Но теперь я могу доверять результатам.

Домашний ноут, кстати, теперь показывает совсем скромные результаты в multiprocessing (а вроде бы те же 4 ядра, но он быстро перегревается и уходит в throttling:

(aiozmq)andrew@tiktaalik2:~/projects/aiozmq (master)$ python benchmarks/simple.py -n 5000 -t 100
 Run tests for 100*5000 iterations: ['aiozmq.rpc', 'core aiozmq', 'single thread raw zmq', 'single thread zmq with poller', 'zmq with threads']

....................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................

RPS calculated as 95% confidence interval
Results for aiozmq.rpc
RPS: 975: [747, 1204],  mean: 1038.325 μs,  standard deviation 115.027 μs

Results for core aiozmq
RPS: 3136: [2176, 4097],    mean: 326.753 μs,   standard deviation 52.423 μs

Results for single thread raw zmq
RPS: 6429: [4138, 8721],    mean: 161.251 μs,   standard deviation 32.622 μs

Results for single thread zmq with poller
RPS: 5461: [3879, 7042],    mean: 187.659 μs,   standard deviation 31.816 μs

Results for zmq with threads
RPS: 6349: [4730, 7967],    mean: 160.318 μs,   standard deviation 22.218 μs

Мораль

Сравнивайте относительные показатели -- современные процессоры слишком сильно отличаются друг от друга. Ноутбук не допускает перегрева, десктоп к нагреванию относится помягче а сервер живет в раскалённом состоянии. Виртуальные сервера на Амазоне -- отдельная тема.

Если делаете benchmarks -- проверьте результаты статистически.

Распространенный в интернете способ: "запустить три раза и выбрать лучший результат" никуда не годится.

Средний результат тоже ни о чём не говорит.

Нужно запустить тест как минимум 33 (а лучше 333) раза и оценить не только среднее время а и среднеквадратичное отклонение как минимум.

Я -- далёкий от математики человек. Как и подавляющее большинство программистов, мне кажется. Изучал в институте, да с тех пор прошло 15 лет и почти всё выветрилось.

Среднеквадратичное отклонение мне мало о чём говорит. Много или мало -- сложно было сказать сразу после того как я начал собирать статистику для своих тестов.

Но я вспомнил что на моём любимом сайте http://elementy.ru в научных статьях часто используют схемки вроде этой:

Доверительный
интервал.

К сожалению сайт элементов стал недоступен из Киева по неизвестной мне причине несколько недель назад. Читаю его через буржуйский VPN. Поэтому буду рассказывать что к чему на примере из википедии.

Столбики -- это средние значения. А красные вертикальные линии на столбиках -- это именно доверительные интервалы, заданные с какой-то погрешностью. Обычно принято использовать 95% если явно не указано другое значение.

По этим красным вертикальным линиям очень наглядно видно, насколько полученное среднее значение измеряемой величины размазано по выборке.

Надеюсь, все поняли основную идею: смотрим на результат теста и обращаем внимание на границы изменчивости измеряемой величины.

P.S.

Мне бы тоже стоило выложить похожую картинку, но я не знаю matplotlib и не могу сделать красивый график.

Помогите, пожалуйста.

Лучший вариант -- pull request на github который позволит запустить python benchmarks/simple.py -n 5000 -t 100 --save-plot=file.png или что-то вроде того.

Upd

Спасибо Артему Дудареву, теперь у меня есть хорошая генерилка графиков:

картинка

воскресенье, 13 апреля 2014 г.

Оценка производительности aiozmq

Сделал "пузомерку" для сравнения производительности aiozmq и просто pyzmq.

aiozmq использует pyzmq в своих внутренностях и стало интересно узнать, какие тормоза добавляет связка aiozmq + asyncio по сравнению с "простыми zmq сокетами".

Тест делался для пары DEALER/ROUTER (RPC) в разных режимах.

Результаты запуска измерителя производительности:

(aiozmq)andrew@tiktaalik2:~/projects/aiozmq (master)$ python benchmarks/simple.py -n 10000
Run tests for 10*10000 iterations: ['aiozmq.rpc', 'core aiozmq', 'single thread raw zmq', 'single thread zmq with poller', 'zmq with threads']
..................................................

Results for aiozmq.rpc
RPS: 2469,   average: 0.405 ms

Results for core aiozmq
RPS: 5064,   average: 0.197 ms

Results for single thread raw zmq
RPS: 9895,   average: 0.101 ms

Results for single thread zmq with poller
RPS: 12574,  average: 0.080 ms

Results for zmq with threads
RPS: 9702,   average: 0.103 ms

zmq шустрее, естественно. Обработка request-response на zmq в одном потоке примерно вдвое быстрее той же работы, которую делает aiozmq на своих транспортах и протоколах, плюс еще asyncio добавляет тормозов.

Даже на нитях (threads) zmq уверенно побеждает. В этом заслуга libzmq, которая создает свой внутренний thread для обработки send и в результате для Питона send получается неблокирующим.

aiozmq.rpc добавляет тормозов по сравнению с aiozmq.core примерно в два раза. Я считаю это приемлемой платой за прозрачную упаковку/распаковку аргументов вызываемой функции, поиск обработчика на стороне сервера, проверку сигнатур для параметров, пробрасывания исключения назад вызывающей стороне.

Если всю эту необходимую работу сделать на zmq -- думаю, получится не сильно быстрее.

Результат

aiozmq.core дает примерно 5000 requests per second, что довольно неплохо.

aiozmq.rpc способен выжать примерно 2500 rps.

То есть если вас устраивает обработка запроса к aiozmq.rpc меньше чем за одни милисекунду -- aiozmq вам подойдёт.

И, самое главное: если на стороне RPC сервера вы делаете запросы в redis, mongo, postgresql, mysql или обращаетесь каким другим внешним для вашего процесса ресурсам -- скорее всего тормоза будут именно в этом месте.

Почему это не очень важно

Да, я знаю что redis неимоверно быстр: показывает 70000+ rps на простых запросах. Но скорее всего вам таких обращений потребуется несколько, и делать вы их будете из питона используя библиотеку вроде asyncio-redis.

Которая добавляет немало приятных плюшек и расплачивается за это производительностью.

Это не значит что за скорость не нужно бороться. Просто для меня aiozmq показала ожидаемые и вполне неплохие результаты. Самый простой путь к ускорению лежит в оптимизации asyncio путём создания optional C Extensions для event loop и selector. Возможно, я этим займусь, или сделают другие Python Core Developers. Как это произошло с модулем io из стандартной библиотеки: после того как его переписали на С в Python 3.2 получили 30% ускорение.

понедельник, 7 апреля 2014 г.

PyCon RU 2014

Приглашаю всех на PyCon RU 2014.

В прошлом году году конференция прошла просто замечательно!
Интересные доклады и приятное место.

Как и в прошлом году конференция будет в загородном учебном центре Иволга (30 км от Екатеринбурга). Зимой там классно. Уверен, летом не хуже.

Много коллег-питонистов, есть с кем поболтать. Лично для меня это и редкая возможность увидеть тех людей, с которыми знаком больше по переписке, а видимся только на конференциях вроде этой.

Не смотря на то что это аж в Екатиренбурге (далеко от Москвы и близко к географическому центру России) -- рекомендую посетить. О том, как было здорово в прошлый раз повторяться не стану.

Если у кого зудит в ягодицах от желания поделиться новым опытом -- заполните  форму на рассмотрение вашего доклада.

Я тоже приеду с лекцией об asyncio, моём опыте по участии в создании этой системы и написании библиотек для.

Может, будет еще один доклад, пока совершенно секретный (жду согласования).


воскресенье, 6 апреля 2014 г.

aiopg -- asyncio библиотека для PostgreSQL

Сделал библиотеку, которая может работать с PostgreSQL под asyncio.

Внутри используется psycopg2 в асинхронном режиме.

Есть connection pool.

Документация -- здесь.

суббота, 5 апреля 2014 г.

asyncio и HTTP

asyncio не умеет работать с HTTP.

Так и было задумано.

asyncio никогда не станет веб-сервером. Он делался как именно event loop для tcp, ssl, unix sockets, pipes и subprocesses. Плюс streaming API.

Веб был сознательно выпилен и теперь то что было лежит в aiohttp. Эта часть просто не дозрела до включения в стандартную библиотеку.

Идея такая:

  • WSGI -- синхронный протокол, для asyncio не подходит.
  • Какой будет новый стандарт -- неясно никому.
  • Пусть для asyncio люди попытаются сделать свои http либы и время покажет у кого получилось лучше.
  • Тогда, возможно, и появится новый стандарт.

Что касается меня то я пытаюсь понять какой именно должен быть API для HTTP server, что там должно быть обязательно и что нужно сознательно исключить.

Сейчас делаем это aiorest

Когда поймём, что получилось хорошо в aiorest -- займемся перенесением удачных решений в aiohttp. Там HTTP server слишком уж неудобный. А нужно что-то типа tornado.web, но более симпатичное и приятное.

четверг, 3 апреля 2014 г.

aiozmq -- поддержка ZeroMQ сокетов в asyncio

Наверное, уже все слышали про asyncio -- новую стандартную библиотеку для асинхронного сетевого программирования.

Естественно, asyncio не умеет работать с ZeroMQ сокетами и никогда не будет уметь.

На днях я выпустил первую версию библиотеки aiozmq, которая устраняет проблему.

aiozmq предоставляет 0MQ event loop совместимый с asyncio и высокоуровневые средства для организации вызовов удалённых процедур aka RPC.

Если интересны подробности -- читайте документацию, она довольно большая и подробная (постарался).