среда, 28 июня 2017 г.

Fast Python. Парсинг ISO дат

Преобразование ISO-даты из строки в объект datetime.datetime (или datetime.date), наверное, одна из самых распространенных и постоянных задач в web-разработке на Python. Количество способов сделать это просто поражает воображение,

In [1]: value = '2017-06-28T16:59:27+0000'

In [2]: import datetime

In [3]: datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z')
Out[3]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=datetime.timezone.utc)

In [4]: from dateutil.parser import parse

In [5]: parse(value)
Out[5]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=tzutc())

In [6]: import dateparser

In [7]: dateparser.parse(value)
Out[7]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=)

Использовались функции: datetime.datetime.strptime, dateutil.parser.parse и dateparser.parse.

А ведь еще есть,

In [8]: import arrow

In [9]: arrow.get(value, 'YYYY-MM-DDTHH:mm:ssZZ')
Out[9]: <Arrow [2017-06-28T16:59:27+00:00]>

In [10]: import maya

In [11]: maya.parse(value)
Out[11]: <MayaDT epoch=1498669167.0>

-//-: arrow.get и maya.parse.

И это только вершина айсберга, потому что библиотек для работ с датами в Python не просто много, а очень много (что говорить, если у Django вообще есть своя parse_datetime функция, основанная на регекспе). У той или иной библиотеки работы с датами есть свои плюсы и особенности, но что делать, если нужно быстро конвертировать строки с датами в стандартные объекты datetime.datetime? Как всегда ответ прост: использовать что-то написанное на C с биндингами в Python, в нашем случае - это ciso8601,

In [12]: ciso8601.parse_datetime(value)
Out[12]: datetime.datetime(2017, 6, 28, 16, 59, 27, tzinfo=)

Результаты %timeit - красноречивы,

In [13]: %timeit datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S%z')
17.9 µs ± 516 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [14]: %timeit parse(value)
99 µs ± 1.1 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [15]: %timeit dateparser.parse(value)
1.61 ms ± 39.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [16]: %timeit ciso8601.parse_datetime(value)
2.08 µs ± 67.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Итого: Если нужно быстро парсить ISO даты: используйте ciso8601.parse_datetime, если нужно быстро парсить не ISO даты: сначала попробуйте стандартный datetime.datetime.strptime, потом dateutil.parser.parse и только в случае особой и острой необходимости dateparser.parse и прочие библиотеки, основанные на нем.

ps. Результаты приведены для Python 3.6.1, на Python 2.7.13 соблюдаются такие же показатели, но в 2.7.13 в стандартном strptime не было возможности разобрать +0000, потому что ValueError: 'z' is a bad directive in format '%Y-%m-%dT%H:%M:%S%z'.

среда, 11 марта 2015 г.

Fast Python. Выпуск 1. Обновление словарей

Привет! Запускаю раздел Fast Python, в котором буду делиться простыми рецептами про то, как ускорить и оптимизировать выполнение кода на Python.

Первый выпуск будет посвящен обновлению данных в словарях. Словари - одни из найболее часто используемых типов данных в Python. И сколько времени я с ними не работаю, у меня никогда не возникал вопрос как правильней обновить данные в словаре. Я для себя всегда отвечал на него, что правильней обновлять используя метод update, но сегодня с утра наткнулся на твит от Брэда Монтгомери и мой мир изменился.

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

Python 3.4.3

In [2]: timeit("d = {}; d.update({'first': 'first', 'second': ['one', 'two', 'three'], 'third': True})", number=1000000)
Out[2]: 0.9664371280086925

In [3]: timeit("d = {}; d['first'] = 'first'; d['second'] = ['one', 'two', 'three']; d['third'] = True", number=1000000)                                      
Out[3]: 0.4200690069992561

Python 2.7.9

In [2]: timeit("d = {}; d.update({'first': 'first', 'second': ['one', 'two', 'three'], 'third': True})", number=1000000)
Out[2]: 1.0035829544067383

In [3]: timeit("d = {}; d['first'] = 'first'; d['second'] = ['one', 'two', 'three']; d['third'] = True", number=1000000)
Out[3]: 0.5186400413513184

Bonus: PyPy 2.4.0

>>>> timeit("d = {}; d.update({'first': 'first', 'second': ['one', 'two', 'three'], 'third': True})", number=1000000)                                               
0.10811400413513184
>>>> timeit("d = {}; d['first'] = 'first'; d['second'] = ['one', 'two', 'three']; d['third'] = True", number=1000000)                                         
0.005925893783569336

Итого: Если вы где-то в коде увидите обновление одиночного или множественных элементов словаря через .update - смело переписывайте этот фрагмент на использование __setitem__.

Fast Python:

data['key'] = 'value'
data['another-key'] = 'another-value'

Slow Python:

data.update({
    'key': 'value',
    'another-key': 'another-value',
})

UPD: Михаил Кривушин (deepwalker) в комментариях объяснил почему так происходит:

По дороге создается еще один словарь, и потом уже из него копируются элементы
https://gist.github.com/Deepwalker/c52a74c26df0707dd303

UPD2: Если ключи словаря не используют специальных символов и являются валидными Python ключами, то лучше использовать синтаксис .update(key='value'), вместо .update({'key': 'value'}):

Python 3.4.3

In [2]: timeit("d = {}; d.update(first='first', second=['one', 'two', 'three'], third=True)", number=1000000)
Out[2]: 0.88018084000214

Python 2.7.9

In [2]: timeit("d = {}; d.update(first='first', second=['one', 'two', 'three'], third=True)", number=1000000)
Out[2]: 0.7820448875427246

PyPy 2.4.0

>>>> timeit("d = {}; d.update(first='first', second=['one', 'two', 'three'], third=True)", number=1000000)
0.010169029235839844

понедельник, 16 февраля 2015 г.

Upgrade your pip & virtualenv now

Странно, что несмотря на очень давний выход pip 6.0 и virtualenv 12.0, многие Python разработчики все еще сидят на более ранних версиях этих незаменимых утилит.

Мой вам совет - обновляйте свой pip & virtualenv сейчас же!

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

[install]
download_cache = /path/to/pip-cache

в ~/.pip/pip.conf, но старый менеджер загрузок был скорее дополнительным, чем полностью готовым к использованию механизмом. Более детальный ход разработки менеджера загрузок хорошо продемонстрирован на GitHub.

Из остального в новом pip ведется проверка версий относительно PEP 440, а значит, что, к сожалению, лучше попрощаться с версиями: X.Y-dev, которые хоть и не попадают на PyPI, но очень часто используются в разработке.

понедельник, 25 августа 2014 г.

Celery воркер зависает на "mingle: searching for neighbors"

Сегодня ВНЕЗАПНО все Celery воркеры перестали принимать любые задачи, при том что RabbitMQ (используется в качестве брокера) очереди были пустыми, а остальные части системы функционировали нормально. Не помогала ни перезагрузка Celery воркеров, ни перезагрузка RabbitMQ сервера.

После недолгого копания по логам проблема была локализирована, любой Celery воркер как будто зависал на моменте поиска соседей, оставляя в логах что-то такое:

2014-08-25 14:29:12 [INFO:celery.worker.consumer] Connected to amqp://guest:**@127.0.0.1:5672//
2014-08-25 14:29:12 [INFO:celery.worker.consumer] mingle: searching for neighbors

Быстрый гуглинг указал на существующую Celery issue, а уже в ней и нашелся ответ на проблему. Оказывается на корневом разделе подошло к концу место (было доступно порядка 150 Мб) и Celery в связке с RabbitMQ зависала не показывая никаких признаков жизни. Все починилось банальной чисткой корневого раздела, но осадок остался и на себя, что не поставил уведомления о заканчивающемся месте на корневом разделе и на Celery, что она никаким образом не пытается обработать эту ситуацию и проблему приходится вычислять окольными путями.

воскресенье, 4 мая 2014 г.

Range для не целых чисел

Почему-то никогда не думал, что rangexrange) понимают только целые числа (в Python 3 ситуация такая же) и функцией нельзя воспользоваться, как например:

range(0., 5., .5)
range(Decimal('0'), Decimal('10'), Decimal('1.5'))

Поэтому пришлось сделать свою замену:

import operator


def arange(start, stop=None, step=None):
    """
    Implement range function not only for integers as Python's builtin
    function, but for Decimals and floats as well.

    Returns generator with arithmetic progession, not list.
    """
    klass = type(start)
    lt_func = operator.lt

    stop = start if stop is None else stop
    start = klass(0) if start == stop else start
    step = klass(1 if step is None else step)

    assert isinstance(stop, klass), (
        'Start and stop limits have different types, {0!r} != {1!r}.'.
        format(type(start).__name__, type(stop).__name__)
    )
    assert step, "Step shouldn't be a zero: {0!r}.".format(step)

    if start < stop and step < 0 or start > stop and step > 0:
        raise StopIteration
    elif start > stop and step < 0:
        lt_func = operator.gt

    while lt_func(start, stop):
        yield start
        start += step

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

Как всегда код оформлен как Gist на GitHub'е, там еще доктесты и юниттесты.