Sprachabhängige Sortierung

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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...
Zuletzt geändert von snafu am Dienstag 13. August 2013, 15:35, insgesamt 1-mal geändert.
Benutzeravatar
diesch
User
Beiträge: 80
Registriert: Dienstag 14. April 2009, 13:36
Wohnort: Brandenburg a.d. Havel
Kontaktdaten:

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))
http://www.florian-diesch.de
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@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. :)
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

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.
Das Leben ist wie ein Tennisball.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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...
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)
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

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.
Das Leben ist wie ein Tennisball.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@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?
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

snafu hat geschrieben:Oder worauf wolltest du jetzt genau hinaus?
Dass ich genauer lesen sollte ...
Das Leben ist wie ein Tennisball.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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. ;)
Antworten