понедельник, 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.