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)
['Андрій', 'Віталій', 'Євген', 'Ігор']

blog comments powered by Disqus