среда, 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'е, там еще доктесты и юниттесты.

четверг, 25 апреля 2013 г.

Миграция с Posterous на Blogger

Как вы, наверное, знаете Twitter сначала купил Posterous, а потом его закрыл. И так как на Posterous'е был мой блог, я перенес все посты из него сюда. Сорри, если RSS фид обновился слишком рьяно.

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

зы. Так как этот блог стал моим единственным, то я сменил его название на более честное. Впредь тут будут появлятся информация и по Flask'у, и по Django, но в большинстве случаев думаю просто по Python'у.