Сортировка списка строк с определенной локалью в python

Я работаю над приложением, которое использует тексты с разных языков, поэтому для просмотра или создания отчетов некоторые тексты (строки) нужно сортировать на определенном языке.

В настоящее время у меня есть обходное взаимодействие с глобальными настройками локали, что плохо, и я не хочу его выпускать:

default_locale = locale.getlocale(locale.LC_COLLATE)

def sort_strings(strings, locale_=None):
    if locale_ is None:
        return sorted(strings)

    locale.setlocale(locale.LC_COLLATE, locale_)
    sorted_strings = sorted(strings, cmp=locale.strcoll)
    locale.setlocale(locale.LC_COLLATE, default_locale)

    return sorted_strings

Официальная документация на языке python явно говорит о том, что сохранение и восстановление - плохая идея, но не дает никаких предложений: http://docs.python.org/library/locale.html#background-details-hints-tips-and-caveats

Ответ 1

Glibc поддерживает API локали с явным состоянием. Вот быстрая оболочка для этого API, сделанная с помощью ctypes.

# -*- coding: utf-8
import ctypes


class Locale(object):
    def __init__(self, locale):
        LC_ALL_MASK = 8127
        # LC_COLLATE_MASK = 8
        self.libc = ctypes.CDLL("libc.so.6")
        self.ctx = self.libc.newlocale(LC_ALL_MASK, locale, 0)



    def strxfrm(self, src, iteration=1):
        size = 3 * iteration * len(src)
        dest =  ctypes.create_string_buffer('\000' * size)
        n = self.libc.strxfrm_l(dest, src, size,  self.ctx)
        if n < size:
            return dest.value
        elif iteration<=4:
            return self.strxfrm(src, iteration+1)
        else:
            raise Exception('max number of iterations trying to increase dest reached')


    def __del__(self):
        self.libc.freelocale(self.ctx)

и короткий тест

locale1 = Locale('C')
locale2 = Locale('mk_MK.UTF-8')

a_list = ['а', 'б', 'в', 'ј', 'ќ', 'џ', 'ш']
import random
random.shuffle(a_list)

assert sorted(a_list, key=locale1.strxfrm) == ['а', 'б', 'в', 'ш', 'ј', 'ќ', 'џ']
assert sorted(a_list, key=locale2.strxfrm) == ['а', 'б', 'в', 'ј', 'ќ', 'џ', 'ш']

что нужно сделать, это реализовать все функции языка, поддерживать строки юникода python (с функциями wchar *, которые, как я полагаю), и автоматически импортировать описания файлов include или что-то

Ответ 2

Вы можете использовать PyICU, чтобы избежать изменения глобальных настроек:

import icu # PyICU

def sorted_strings(strings, locale=None):
    if locale is None:
       return sorted(strings)
    collator = icu.Collator.createInstance(icu.Locale(locale))
    return sorted(strings, key=collator.getSortKey)

Пример:

>>> L = [u'sandwiches', u'angel delight', u'custard', u'éclairs', u'glühwein']
>>> sorted_strings(L)
['angel delight', 'custard', 'glühwein', 'sandwiches', 'éclairs']
>>> sorted_strings(L, 'en_US')
['angel delight', 'custard', 'éclairs', 'glühwein', 'sandwiches']

Недостаток: зависимость от библиотека PyICU; поведение немного отличается от locale.strcoll.


Я не знаю, как получить функцию locale.strxfrm, учитывая имя локали, не изменяя ее глобально. В качестве взлома вы можете запустить свою функцию в другом дочернем процессе:

pool = multiprocessing.Pool()
# ...
pool.apply(locale_aware_sort, [strings, loc])

Недостаток: может быть медленным, ресурсный голод


Использование обычного threading.Lock не будет работать, если вы не сможете управлять каждым местом, где функции локального распознавания (они не ограничены модулем locale, например, re) могут быть вызваны из нескольких потоков.


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

Недостаток: не чистый Python

Ответ 3

Решение ctypes прекрасное, но если кто-то в будущем захочет просто изменить ваше исходное решение, вот как это сделать:

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

from contextlib import contextmanager
import locale

@contextmanager
def changedlocale(newone):
    old_locale = locale.getlocale(locale.LC_COLLATE)
    try:
        locale.setlocale(locale.LC_COLLATE, newone)
        yield locale.strcoll
    finally:
        locale.setlocale(locale.LC_COLLATE, old_locale)

def sort_strings(strings, locale_=None):
    if locale_ is None:
        return sorted(strings)

    with changedlocale(locale_) as strcoll:
        return sorted(strings, cmp=strcoll)

    return sorted_strings

Это обеспечивает чистое восстановление исходного языка - если вы не используете потоки.