Seite 1 von 1

Sprachabhängige Sortierung

Verfasst: Dienstag 13. August 2013, 09:37
von snafu
Guten Tag.

Ich möchte je nach eingestelltem Locale eine sprachabhängige Sortierung haben. Mir ist klar, dass alles was mit Locales zu tun hat, ein heikles Thema ist - zumindest wenn man dafür die globale Einstellung von Python kurz ändern muss.

Eine Sortierung ohne umgestelltem Locale sähe ja so aus:

Code: Alles auswählen

>>> items = ['bamm', 'bumm', 'bämm']
>>> print ' '.join(sorted(items))
bamm bumm bämm
Er behält also quasi die Reihenfolge bei. Ich hätte aber gerne das "bämm" hinter dem "bamm".

Meine erste Idee war, das `locale`-Modul zu benutzen. Um den Code noch halbwegs aufzuhübschen hatte ich mir dann sowas gebastelt:

Code: Alles auswählen

In [1]: import locale

In [2]: %paste
class _DefaultLocale(object):
    def __init__(self, category):
        self.category = category
        self.old_locale = locale.getlocale(category)
        self.default_locale = locale.getdefaultlocale()

    def __enter__(self):
        locale.setlocale(self.category, self.default_locale)

    def __exit__(self, *unused):
        locale.setlocale(self.category, self.old_locale)

## -- End pasted text --

In [3]: items = ['bamm', 'bumm', 'bämm']

In [4]: with _DefaultLocale(locale.LC_COLLATE):
   ...:     print ' '.join(sorted(items, key=locale.strxfrm))
   ...:     
bamm bämm bumm
Das sieht erstmal (den Umständen entsprechend) gut aus, aber scheint für Unicode-Strings kaputt zu sein:

Code: Alles auswählen

In [5]: items = [u'bamm', u'bumm', u'bämm']

In [6]: with _DefaultLocale(locale.LC_COLLATE):
    print ' '.join(sorted(items, key=locale.strxfrm))
   ...:     
---------------------------------------------------------------------------
UnicodeEncodeError                        Traceback (most recent call last)
<ipython-input-6-bb4ce974ad48> in <module>()
      1 with _DefaultLocale(locale.LC_COLLATE):
----> 2     print ' '.join(sorted(items, key=locale.strxfrm))
      3 

UnicodeEncodeError: 'ascii' codec can't encode character u'\xe4' in position 1: ordinal not in range(128)
Gibt es eine Möglichkeit, das beschriebene Ziel mit vertretbarem Aufwand auch für Unicode-Strings zu erreichen oder müsste ich in diesem Fall definitv auf PyICU ausweichen (siehe: http://stackoverflow.com/a/3413436)?

EDIT: Schon wohl nur Python 2.x zu betreffen. Python 3.x nutzt intern `wcsxfrm()` und kommt dementsprechend mit Unicode klar. Zwar sind dort Bytes verboten, aber das ist ja noch einigermaßen ok. Ich überlege, ob ich einfach `wcsxfrm()` mit `ctypes` wrappe, um die Abhängigkeit zu PyICU maximal optional zu machen. Werde berichten...

Re: Sprachabhängige Sortierung

Verfasst: Dienstag 13. August 2013, 15:32
von diesch
Du kannst locale.strcoll als Sortierfunktion für sorted benutzen:

Code: Alles auswählen

with _DefaultLocale(locale.LC_COLLATE):
    print ' '.join(sorted(items, cmp=locale.strcoll))

Alternativ kannst du auch die Unicode-Strings erst in Byte-Strings umwandeln:

Code: Alles auswählen

with _DefaultLocale(locale.LC_COLLATE):
    l = map(lambda x: x.encode('utf-8'), items)
    print ' '.join(sorted(l,  key=locale.strxfrm))

Re: Sprachabhängige Sortierung

Verfasst: Dienstag 13. August 2013, 16:19
von snafu
@diesch: Funktioniert. Danke.

Hab jetzt sowas gebastelt:

Code: Alles auswählen

if sys.version_info < (3, 0):
    def _sortkey(s):
        if isinstance(s, unicode):
            s = s.encode('utf-8')
        return locale.strxfrm(s.lower())
else:
    def _sortkey(s):
        return locale.strxfrm(s.lower())
Dies wird dann halt als `key` übergeben. `strcoll()` möchte ich eigentlich nicht benutzen, da es wohl eher für das veraltete `cmp`-Interface gedacht ist. `cmp` ist in Python 3.x aber aus `sorted()` entfernt worden. Und mein Code soll in beiden "Welten" laufen.

Aber ist ja schön, dass ich mir die Abhängigkeit zu PyICU damit sparen kann. An so eine simple Lösung hatte ich irgendwie gar nicht gedacht. :oops:

EDIT: `functools.cmp_to_key(locale.strcoll)` ist allerings auch ganz nett... ^^

Code: Alles auswählen

sortkey = functools.cmp_to_key(locale.strcoll)
with _DefaultLocale(locale.LC_COLLATE):
    items = sorted(items, key=sortkey)
# ...
Habe das in der zuletzt beschriebenen Art in meinem Code verwendet. Nochmals danke. :)

Re: Sprachabhängige Sortierung

Verfasst: Dienstag 13. August 2013, 21:10
von EyDu
Hallo.

Mal eine unqualifizierte Vermutung: die Lösung über ``encode`` scheint mir auf recht wackeligen Beinen zu stehen. ``locale.strxfrm`` müsste nämlich das verwendete Encoding kennen. Entweder wird der Vergleich also auf Bytes runtergebrochen (was dann sicher nicht immer das korrekte Ergebnis liefern würde) oder die Kodierung stimmt zufällig mit der des Systems/Terminals/was-auch-immer überein. Dann müsste man die Kodierung noch anpassen.

Bei allen gezeigten Lösungen, bedingt durch ``_DefaultLocale``, bzw. durch das modulweite setzen der Sprache, besteht aber noch immer das Problem, dass die ganze Sache nicht threadsicher ist. Ein Schutz über Locks in _DefaultLocale reicht dazu auch noch nicht aus, da noch immer andere Module in locale rumwerkeln könnten. Ich würde daher auf die saubere PyICU-Lösung setzen, außer ich könnte garantieren, dass weder meine Module, noch irgendwelche Fremdmodule, in locale eingreifen.

Re: Sprachabhängige Sortierung

Verfasst: Mittwoch 14. August 2013, 07:17
von snafu
Ja, das mit der nicht vorhandenen Threadsicherheit ist mir bewusst. Daher auch eingangs meine Erwähnung, dass ein Umstellen der globalen Locale-Einstellung immer etwas heikel ist. Ich hab im eigentlichen Code eine Option eingebaut, wo der User wählen kann, ob er ein automatisches Sortieren haben möchte, oder nicht. Bei Bedarf kann man also auch einen anderen Weg der Sortierung gehen und das Ergebnis dann in mein Modul "schmeißen". Ich werde diesbezüglich wohl auch noch eine entsprechende Warnung in den Docstring der betroffenen Funktion setzen. Es geht dabei übrigens um das in meiner Signatur verlinkte Projekt "shcol", falls sich jemand für den Zusammenhang interessiert...

Re: Sprachabhängige Sortierung

Verfasst: Mittwoch 14. August 2013, 08:43
von BlackJack
Nur mal so am Rande eine Alternative für einen Contextmanager ohne eigene Klasse:

Code: Alles auswählen

import locale
from contextlib import contextmanager


@contextmanager
def _default_locale(category):
    old_locale = locale.setlocale(category, locale.getdefaultlocale())
    try:
        yield
    finally:
        locale.setlocale(category, old_locale)

Re: Sprachabhängige Sortierung

Verfasst: Mittwoch 14. August 2013, 11:26
von EyDu
Stimmt, die Probleme mit den globalen Einstellungen hattest du gleich im ersten Satz erwähnt. Habe ich wohl überlesen. Das klärt aber immer noch nicht die Frage, ob die encode-Lösung tatsächlich in allen Fällen ein korrektes Ergebnis liefert. Die beliebige Wahl der Kodierung finde ich noch immer bedenklich.

Re: Sprachabhängige Sortierung

Verfasst: Mittwoch 14. August 2013, 14:08
von snafu
@EyDu: Aus dem Grund bin ich ja auch zu `strcoll()` gewechselt (wie geschrieben), wo man selbständig nichts mehr de- oder encoden muss. `strcoll()` "frisst" unter Python 2.x (Byte-)Strings und Unicode und unter Python 3.x (Unicode-)Strings. Ich sehe derzeit keine wesentlichen Nachteile darin. Oder worauf wolltest du jetzt genau hinaus?

Re: Sprachabhängige Sortierung

Verfasst: Mittwoch 14. August 2013, 14:25
von EyDu
snafu hat geschrieben:Oder worauf wolltest du jetzt genau hinaus?
Dass ich genauer lesen sollte ...

Re: Sprachabhängige Sortierung

Verfasst: Mittwoch 14. August 2013, 14:47
von snafu
EyDu hat geschrieben:
snafu hat geschrieben:Oder worauf wolltest du jetzt genau hinaus?
Dass ich genauer lesen sollte ...
Einmal haste noch, dann musste einen ausgeben. ;)