Сортировка списка строк с определенной локалью в 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

3 ответа

Решение

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 *) и автоматически импортировать определения включаемых файлов или что-то в этом роде.

Вы можете использовать коллатер 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

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

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

Другие вопросы по тегам