суббота, 14 февраля 2015 г.

vagga

Паша Коломиец работает над проектом vagga.

Коротко говоря, это система виртуализации на linux контейнерах, предназначенная для разработки программ.

Выполняет роль, в чем-то похожую на virtualenv, но со своими отличиями и особенностями.

Рекомендую прочесть свежую Пашину статью, в которой он даёт обзор возможностей vagga и показывает преимущества над virtualenv.

P.S. Я лично к проекту никакого отношения не имею, поэтому все вопросы, пожалуйста, задавайте сразу Паше.


пятница, 16 января 2015 г.

aiohttp 0.14

Выпустил новую версию библиотеки.

Из вкусного -- Web Sockets для aiohttp.web серверов, оптимизация скорости работы, множество мелких улучшений.

Полный список изменений -- здесь.

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