понедельник, 16 июля 2018 г.

Настройка Sublime Text 3 для Python разработки

Выбор редактора (среды для разработки), наверное, главное событие в жизни каждого разработчика. Я тоже поначалу не мог долго определиться какой редактор использовать, но потом, лет 8-9 назад установил Sublime Text и все - вопрос с редактором был закрыт раз и навсегда. Да, за эти года я иногда пробовал научиться работать в vim, но не смог, ну а на новомодные Atom, PyCharm или VS Code я вообще не смотрел. Поэтому я хотел бы рассказать про то, как я использую Sublime Text 3 и почему мне не нужен IDE в 2018 году.

Главная причина, почему мне не нужен IDE - это, конечно, база расширений для Саблайма, которая находится на packagecontrol.io и покрывает любой из языков, на которых я программирую. Так как блог о Python, то я расскажу только о расширениях для Python, но у меня также есть установленные плагины для JavaScript, flow, TypeScript, Elm и Rust.

В Python же все начинается с установки MagicPython от Юрия Селиванова, расширения, которое добавляет поддержку подсветки всех всех новомодных фич Python, и хоть авторы Sublime Text пытаются улучшать поддержку базового Python от релиза к релизу, нам это не нужно после установки и настройки MagicPython.

Дальше. Было бы неплохо иметь автодополнение для Python, да? Поэтому следующим делом, устанавливаем SublimeJEDI от Сережи Русских. Благодаря движку JEDI, который используется в IPython, к примеру, на выходе мы получаем автодополнение нашей мечты. Но для этого в настройках проекта нам нужно сделать две вещи,

  1. Задать путь к python_interpreter, в общем случае это "~/Projects/<project>/.venv/bin/python", чтоб JEDI использовал все зависимости, установленные в проекте
  2. Настроить python_paths. Опять же в общем случае это ["~/Projects/<project>"]

Все, теперь мы получили автодополнение к любому import стейтменту и много других фич, опять же благодаря движку JEDI.

Идем дальше. Линтинг. Тут нам нужны SublimeLinter-flake8 и SublimeLinter-contrib-mypy (а куда без type annotations в 2018 то году?). По дефолту, они будут пытаться использовать стандартный Python, установленный в системе, но так как в моем случае набор flake8 плагинов от проекта к проекту отличается, я задаю executable для этих линтеров в файле настройки проекта (<project>.sublime-project) как,

  1. "SublimeLinter.linters.flake8.executable": "~/Projects/<project>/.venv/bin/flake8" для настройки SublimeLinter-flake8
  2. "SublimeLinter.linters.mypy.executable": "~/Projects/<project>/.venv/bin/mypy" для настройки SublimeLinter-contrib-mypy

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

По большому счету с главными расширениями, которые превращают Sublime Text 3 в удобную среду разработки на Python мы закончили, но мне также хотелось бы упомянуть еще некоторые плагины, которые могут точно пригодиться,

  • sublack - автоформатирование Python файлов при помощи black. Нужная вещь, когда вы работаете в команде и не хотите на код ревью тратить время о спорах насчет форматирования Python кода.
  • editorconfig-sublimetext - также в 2018 году не зачем спорить о табах vs пробелах или отступах. Единократно кладем .editorconfig в корень проекта, а дальше Sublime Text 3 автоматически применяет необходимые настройки форматирования для любых файлов.
  • DocBlockr - упрощает работу с комментариями в коде. Конечно, этот плагин больше применим для JavaScript кода, но и для Python иногда бывает полезен.
  • GitGutter - просмотр того, как изменился блок кода, прямо в Sublime Text. Очень нужная вещь, для разработчиков, которые не любят бессмысленный закомментированный старый код.
  • Terminus - и хоть я постоянно держу открытым Terminal.app иногда бывает удобно открыть терминал прямо в соседней вкладке Sublime Text.

Ну и напоследок чуть о внешнем виде. Все это время я использую схему подсветки кода Tomorrow-Night, так что очень был рад, когда настроил Boxy Theme для внешнего вида Sublime Text, используя это же цветовое решение. В итоге, мой Sublime Text выглядит следующим образом,

А вот шаблон настроек, который я использую для своих проектов,

{
    "folders":
    [
        {
            "file_exclude_patterns": [
                ".install*",
                "*.lock",
                "*-lock.json"
            ],
            "folder_exclude_patterns": [
                ".*cache",
                "node_modules"
            ],
            "path": "/Users/playpauseandstop/Projects/<project>"
        }
    ],
    "settings": {
        "python_interpreter": "/Users/playpauseandstop/Projects/<project>/.venv/bin/python",
        "python_paths": [
            "/Users/playpauseandstop/Projects/<project>"
        ],
        "SublimeLinter.linters.flake8.executable": "/Users/playpauseandstop/Projects/<project>/.venv/bin/flake8",
        "SublimeLinter.linters.mypy.executable": "/Users/playpauseandstop/Projects/<project>/.venv/bin/mypy"
    }
}

Вот и получается, что Sublime Text 3 с таким набором расширений - это прекрасный редактор для программирования на Python.

воскресенье, 1 июля 2018 г.

Fast Python. В поисках медленного кода при помощи стандартного профайлера

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

Сразу оговорюсь, что есть намного более универсальные библиотеки для профайлинга Python приложений, как например, vprof или pyflame, но я хотел бы рассказать именно про cProfile.

Итак, что надо сделать, чтоб отпрофилировать наш код? На самом деле просто запомнить, как запускать профайлер:

$ python -m cProfile -o script.prof script.py

Ну а в Python 3.7 все стало еще приятней, потому что появилась возможность задавать -m для профайлера как:

$ python -m cProfile -o project.prof -m project

После того как скрипт выполнится, мы получим script.prof файл, который теперь можно эксплорить при помощи KCacheGrind. Для этого нужно перевести его в формат, который понимает KCacheGrind, что делает тулза под названием: pyprof2calltree.

$ pyprof2calltree -i script.prof -k

переконвертирует script.prof в дерево вызовов и откроет приятный глазу интерфейс KCacheGrind.

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

  1. В начале project/__main__.py создаю профайлер и запускаю его, если в окружении есть переменная USE_PROFILER
  2. Оборачиваю web.run_app(...) в try/finally и в finally печатаю содержимое профайлера в файл
  3. Потом запускаю приложение как USE_PROFILER=1 python -m project
  4. И наконец делаю нагрузку на необходимый URL при помощи wrk, например как, wrk -t 4 -c 100 -d 30s --latency http://<URL>/api/slow
  5. Все теперь можно выключать приложение и запускать KCacheGrind

Если теперь перевести это все на код, то получится вот такой апдейт для project/__main__.py,

import cProfile
import os

from app import create_app


USE_PROFILER = os.environ.get('USE_PROFILER') == '1'
profiler = cProfiler.Profile()


if __name__ == '__main__':
    if USE_PROFILER:
        profiler.enable()

    try:
        sys.exit(web.run_app(create_app()))
    finally:
        if USE_PROFILER:
            filename = 'project.prof'
            print(f'Dumping profiler to {filename}')
            profiler.dump_stats(filename)
            profiler.disable()

В итоге, в KCacheGrind, находим интересующую вьюху (на моем скриншоте это retrieve_tweets) и ищем почему же она работает медленно.

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

среда, 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, но очень часто используются в разработке.