10 марта 2019 г.

Сортировка списка с unicode strings

Думал, что после стольких лет Python уже не удивит меня, однако пословица Век живи, век учись стала для меня как никогда актуальной вчера. Задача была весьма простая: отсортировать список с строками, где строка - это украинское имя, то есть вроде бы все должно быть предельно просто используя Python 3.7.2:

data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
assert sorted(data) == ['Андрій', 'Віталій', 'Євген', 'Ігор']

Но нет, not so fast!

$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data)
['Євген', 'Ігор', 'Андрій', 'Віталій']

Интересно. Значит что-то не так с локалью. Нужно установить правильную локаль и попробовать еще раз.

$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> locale.setlocale(locale.LC_COLLATE, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data)
['Євген', 'Ігор', 'Андрій', 'Віталій']

Хм. Странно. Значит нужно пойти почитать доку locale и найти там функцию, которая будет делать locale compare. Точно же есть такая.

$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import locale
>>> locale.setlocale(locale.LC_ALL, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> locale.setlocale(locale.LC_COLLATE, 'uk_UA.UTF-8')
'uk_UA.UTF-8'
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data, key=locale.strxfrm)
['Ігор', 'Євген', 'Андрій', 'Віталій']
>>> import functools
>>> sorted(data, key=functools.cmp_to_key(locale.strcoll))
['Ігор', 'Євген', 'Андрій', 'Віталій']

Та ладно. Не верю. Все должно работать. Аргх.

После того, как эмоции уляглись, вспоминаю про ICU и думаю, ну ок, точно есть биндинги ICU для Python и там должна быть функция, которую можно будет скормить в key для правильной сортировки значений в списке.

  1. Устанавливаем ICU в систему,
    • Для macOS:
      $ brew install icu4c
      $ export PATH="/usr/local/opt/icu4c/bin:$PATH"
    • Для Ubuntu Linux:
      # apt install libicu-dev icu-devtools
  2. Устанавливаем PyICU,
    $ poetry add PyICU
    или по старинке:
    $ pip install PyICU
$ python
Python 3.7.2 (default, Jan  4 2019, 12:23:06) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import icu
>>> collator = icu.Collator.createInstance(icu.Locale('uk_UA.UTF-8'))
>>> data = ['Андрій', 'Ігор', 'Євген', 'Віталій']
>>> sorted(data, key=collator.getSortKey)
['Андрій', 'Віталій', 'Євген', 'Ігор']

Фух! Работает! Так что да, чистая правда: век живи, век учись!

ps. В комментариях @xnull поделился еще одним способом сортировки при помощи pyuca библиотеки, которая не требует установки в систему никаких дополнительных зависимостей.

$ poetry add pyuca  # pip install pyuca
$ python
Python 3.7.2 (default, Jan  2 2019, 13:30:18) 
[Clang 10.0.0 (clang-1000.10.44.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pyuca
>>> coll = pyuca.Collator()
>>> data = ['Євген', 'Ігор', 'Андрій', 'Віталій']
>>> sorted(data, key=coll.sort_key)
['Андрій', 'Віталій', 'Євген', 'Ігор']

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_package_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_package_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