Wie sortiere ich Objekte in Python nach mehreren Schlüsseln?

91

Oder wie kann ich eine Liste von Wörterbüchern praktisch nach mehreren Schlüsseln sortieren?

Ich habe eine Liste von Diktaten:

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
 {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
 {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
 {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
 {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
 {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

und ich muss eine Mehrschlüsselsortierung verwenden, die von Total_Points umgekehrt und dann nicht von umgekehrt wird TOT_PTS_Misc.

Dies kann an der Eingabeaufforderung wie folgt erfolgen:

a = sorted(b, key=lambda d: (-d['Total_Points'], d['TOT_PTS_Misc']))

Aber ich muss dies durch eine Funktion ausführen, bei der ich die Liste und die Sortiertasten übergebe. Zum Beispiel def multikeysort(dict_list, sortkeys):.

Wie kann die Lambda-Zeile verwendet werden, die die Liste für eine beliebige Anzahl von Schlüsseln sortiert, die an die Multikeysort-Funktion übergeben werden, und berücksichtigt werden, dass die Sortierschlüssel eine beliebige Anzahl von Schlüsseln haben können und diejenigen, die umgekehrte Sortierungen benötigen, identifiziert werden mit einem '-' davor?

simi
quelle

Antworten:

70

Diese Antwort funktioniert für jede Art von Spalte im Wörterbuch - die negierte Spalte muss keine Zahl sein.

def multikeysort(items, columns):
    from operator import itemgetter
    comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else
                  (itemgetter(col.strip()), 1)) for col in columns]
    def comparer(left, right):
        for fn, mult in comparers:
            result = cmp(fn(left), fn(right))
            if result:
                return mult * result
        else:
            return 0
    return sorted(items, cmp=comparer)

Sie können es so nennen:

b = [{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0},
     {u'TOT_PTS_Misc': u'Foster, Toney', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lawson, Roman', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Lempke, Sam', u'Total_Points': 80.0},
     {u'TOT_PTS_Misc': u'Gnezda, Alex', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Kirks, Damien', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Worden, Tom', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Korecz, Mike', u'Total_Points': 78.0},
     {u'TOT_PTS_Misc': u'Swartz, Brian', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Burgess, Randy', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Smugala, Ryan', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Harmon, Gary', u'Total_Points': 66.0},
     {u'TOT_PTS_Misc': u'Blasinsky, Scott', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Carter III, Laymon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Coleman, Johnathan', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Venditti, Nick', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Blackwell, Devon', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Kovach, Alex', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Bolden, Antonio', u'Total_Points': 60.0},
     {u'TOT_PTS_Misc': u'Smith, Ryan', u'Total_Points': 60.0}]

a = multikeysort(b, ['-Total_Points', 'TOT_PTS_Misc'])
for item in a:
    print item

Versuchen Sie es mit einer der negierten Spalten. Sie sehen die umgekehrte Sortierreihenfolge.

Weiter: Ändern Sie es so, dass keine zusätzliche Klasse verwendet wird.


2016-01-17

Ich lasse mich von dieser Antwort inspirieren. Was ist der beste Weg, um den ersten Artikel aus einer iterierbaren Übereinstimmung mit einer Bedingung zu erhalten? Ich habe den Code gekürzt:

from operator import itemgetter as i

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, cmp=comparer)

Falls Sie Ihren Code knapp mögen.


Später 17.01.2016

Dies funktioniert mit Python3 (wodurch das cmpArgument beseitigt wurde sort):

from operator import itemgetter as i
from functools import cmp_to_key

def cmp(x, y):
    """
    Replacement for built-in function cmp that was removed in Python 3

    Compare the two objects x and y and return an integer according to
    the outcome. The return value is negative if x < y, zero if x == y
    and strictly positive if x > y.

    https://portingguide.readthedocs.io/en/latest/comparisons.html#the-cmp-function
    """

    return (x > y) - (x < y)

def multikeysort(items, columns):
    comparers = [
        ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
        for col in columns
    ]
    def comparer(left, right):
        comparer_iter = (
            cmp(fn(left), fn(right)) * mult
            for fn, mult in comparers
        )
        return next((result for result in comparer_iter if result), 0)
    return sorted(items, key=cmp_to_key(comparer))

Inspiriert von dieser Antwort Wie soll ich in Python 3 eine benutzerdefinierte Sortierung durchführen?

hughdbrown
quelle
Dies funktioniert am besten, da ich die Umkehrung für alle Schlüssel oder Spalten verwenden kann. Danke dir!
Simi
Das funktioniert also gut. Ich rufe meine Funktion mit der Liste und der Zeichenfolge als Parameter auf. Ich teile zuerst die Zeichenfolge und rufe dann den Multikeysort mit der Liste und der Liste der Schlüssel aus der geteilten Zeichenfolge auf. Es spielt keine Rolle, welches Element in der Zeichenfolge am Anfang des Spaltennamens das '-' enthält, da es entweder mit dem Element oder mit allen Elementen funktioniert. Genial. Danke dir.
Simi
2
Danke, du hast meinen Tag gerettet!
Sander van Leeuwen
4
cmp()ist für Python3 nicht verfügbar, daher musste ich es selbst definieren, wie hier erwähnt: stackoverflow.com/a/22490617/398514
pferate
8
@hughdbrown: Sie haben das cmpSchlüsselwort entfernt, aber die cmp()Funktion wird noch 4 Zeilen weiter oben verwendet. Ich habe es mit 3.2, 3.3, 3.4 und 3.5 versucht, alle sind beim Funktionsaufruf fehlgeschlagen, weil cmp()nicht definiert. Der dritte Punkt hier ( docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons ) erwähnt die Behandlung cmp()als weg.
pferate
52

Dieser Beitrag enthält einen guten Überblick über verschiedene Techniken, um dies zu tun. Wenn Ihre Anforderungen einfacher sind als "Full Bidirectional Multikey", werfen Sie einen Blick darauf. Es ist klar, dass die akzeptierte Antwort und der Blog-Beitrag, auf den ich gerade verwiesen habe, sich gegenseitig beeinflusst haben, obwohl ich nicht weiß, in welcher Reihenfolge.

Für den Fall, dass der Link stirbt, finden Sie hier eine sehr kurze Zusammenfassung der Beispiele, die oben nicht behandelt wurden:

mylist = sorted(mylist, key=itemgetter('name', 'age'))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), k['age']))
mylist = sorted(mylist, key=lambda k: (k['name'].lower(), -k['age']))
Scott Stafford
quelle
Soweit ich das beurteilen kann, verwendet stygianvision meinen Code und gibt keine Gutschrift. Google fürresult = cmp(fn(left), fn(right))
hughdbrown
4
Vielen Dank für die Zusammenfassung, Link ist jetzt tatsächlich tot. :)
Amyth
46

Ich weiß, dass dies eine ziemlich alte Frage ist, aber keine der Antworten erwähnt, dass Python eine stabile Sortierreihenfolge für seine Sortierroutinen wie list.sort()und garantiert sorted(), was bedeutet, dass Elemente, die gleich sind, ihre ursprüngliche Reihenfolge beibehalten.

Dies bedeutet, dass das Äquivalent von ORDER BY name ASC, age DESC(unter Verwendung der SQL-Notation) für eine Liste von Wörterbüchern wie folgt ausgeführt werden kann:

items.sort(key=operator.itemgetter('age'), reverse=True)
items.sort(key=operator.itemgetter('name'))

Beachten Sie, wie die Elemente zuerst nach dem Attribut "kleiner" age(absteigend) und dann nach dem Attribut "groß" sortiert werden name, was zur korrekten endgültigen Reihenfolge führt.

Das Umkehren / Invertieren funktioniert für alle bestellbaren Typen, nicht nur für Zahlen, die Sie durch Setzen eines Minuszeichens negieren können.

Und aufgrund des in (zumindest) CPython verwendeten Timsort-Algorithmus ist dies in der Praxis eigentlich ziemlich schnell.

Wouter Polsterlee
quelle
2
Sehr schön. Für moderate Datensätze, bei denen das mehrfache Sortieren des Satzes keine Rolle spielt, ist dies super cool! Wie Sie hervorheben, müssen Sie die Python-Sortierung im Vergleich zur SQL-Sortierung umkehren. Vielen Dank.
Greg
Die zweite Sortierung bricht das Ergebnis der ersten. Komisch, dass keiner der Aufsteiger es bemerkt hat.
Vulkan
9
Es ist lustig, dass Sie nicht bemerkt haben, dass das primäre Sortierkriterium das letzte ist, wie in meinem Beispiel gezeigt und im anderen Kommentar explizit erwähnt, um es sehr deutlich zu machen, falls Sie es nicht bemerkt haben.
Wouter Bolsterlee
24
def sortkeypicker(keynames):
    negate = set()
    for i, k in enumerate(keynames):
        if k[:1] == '-':
            keynames[i] = k[1:]
            negate.add(k[1:])
    def getit(adict):
       composite = [adict[k] for k in keynames]
       for i, (k, v) in enumerate(zip(keynames, composite)):
           if k in negate:
               composite[i] = -v
       return composite
    return getit

a = sorted(b, key=sortkeypicker(['-Total_Points', 'TOT_PTS_Misc']))
Alex Martelli
quelle
Beeindruckend! Das ist fantastisch. Es funktioniert großartig. Ich bin so ein Neuling, dass ich das Gefühl habe, das alles nie zu wissen. Das war auch schnell. Vielen Dank.
Simi
Was aber, wenn die an den Sortierschlüssel ausgewählten Schlüssel eine Zeichenfolge sind, z. B. '-Total_Points, TOT_PTS_Misc'?
Simi
1
Dann könnten Sie die Zeichenfolge in ein Array aufgeteilt zuerst durch den Aufrufsome_string.split(",")
Jason Creighton
Danke dir. Nachdem ich bereits einen Kommentar abgegeben hatte, wurde mir klar, dass ich den String teilen kann. DOH!
Simi
2
Was aber, wenn Sie den Zeichenfolgenwert anstelle des Zahlenwerts negieren? Ich denke nicht, dass das funktionieren würde.
Nick Perkins
5

Ich verwende das Folgende zum Sortieren eines 2D-Arrays nach mehreren Spalten

def k(a,b):
    def _k(item):
        return (item[a],item[b])
    return _k

Dies könnte erweitert werden, um an einer beliebigen Anzahl von Elementen zu arbeiten. Ich denke eher, dass es besser ist, ein besseres Zugriffsmuster auf Ihre sortierbaren Schlüssel zu finden, als einen ausgefallenen Komparator zu schreiben.

>>> data = [[0,1,2,3,4],[0,2,3,4,5],[1,0,2,3,4]]
>>> sorted(data, key=k(0,1))
[[0, 1, 2, 3, 4], [0, 2, 3, 4, 5], [1, 0, 2, 3, 4]]
>>> sorted(data, key=k(1,0))
[[1, 0, 2, 3, 4], [0, 1, 2, 3, 4], [0, 2, 3, 4, 5]]
>>> sorted(a, key=k(2,0))
[[0, 1, 2, 3, 4], [1, 0, 2, 3, 4], [0, 2, 3, 4, 5]]
mumrah
quelle
2

Ich hatte heute ein ähnliches Problem - ich musste Wörterbuchelemente nach absteigenden numerischen Werten und nach aufsteigenden Zeichenfolgenwerten sortieren. Um das Problem widersprüchlicher Richtungen zu lösen, habe ich die ganzzahligen Werte negiert.

Hier ist eine Variante meiner Lösung - wie für OP

sorted(b, key=lambda e: (-e['Total_Points'], e['TOT_PTS_Misc']))

Sehr einfach - und wirkt wie ein Zauber

[{'TOT_PTS_Misc': 'Chappell, Justin', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Russo, Brandon', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Utley, Alex', 'Total_Points': 96.0},
 {'TOT_PTS_Misc': 'Foster, Toney', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lawson, Roman', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Lempke, Sam', 'Total_Points': 80.0},
 {'TOT_PTS_Misc': 'Gnezda, Alex', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Kirks, Damien', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Korecz, Mike', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Worden, Tom', 'Total_Points': 78.0},
 {'TOT_PTS_Misc': 'Burgess, Randy', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Harmon, Gary', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Smugala, Ryan', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Swartz, Brian', 'Total_Points': 66.0},
 {'TOT_PTS_Misc': 'Blackwell, Devon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Blasinsky, Scott', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Bolden, Antonio', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Carter III, Laymon', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Coleman, Johnathan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Kovach, Alex', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Smith, Ryan', 'Total_Points': 60.0},
 {'TOT_PTS_Misc': 'Venditti, Nick', 'Total_Points': 60.0}]
Vulkan
quelle
0
from operator import itemgetter
from functools import partial

def _neg_itemgetter(key, d):
    return -d[key]

def key_getter(key_expr):
    keys = key_expr.split(",")
    getters = []
    for k in keys:
        k = k.strip()
        if k.startswith("-"):
           getters.append(partial(_neg_itemgetter, k[1:]))
        else:
           getters.append(itemgetter(k))

    def keyfunc(dct):
        return [kg(dct) for kg in getters]

    return keyfunc

def multikeysort(dict_list, sortkeys):
    return sorted(dict_list, key = key_getter(sortkeys)

Demonstration:

>>> multikeysort([{u'TOT_PTS_Misc': u'Utley, Alex', u'Total_Points': 60.0},
                 {u'TOT_PTS_Misc': u'Russo, Brandon', u'Total_Points': 96.0}, 
                 {u'TOT_PTS_Misc': u'Chappell, Justin', u'Total_Points': 96.0}],
                "-Total_Points,TOT_PTS_Misc")
[{u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Chappell, Justin'}, 
 {u'Total_Points': 96.0, u'TOT_PTS_Misc': u'Russo, Brandon'}, 
 {u'Total_Points': 60.0, u'TOT_PTS_Misc': u'Utley, Alex'}]

Das Parsen ist etwas fragil, lässt aber zumindest eine variable Anzahl von Leerzeichen zwischen den Schlüsseln zu.

Torsten Marek
quelle
Wenn ich jedoch das zweite Element in der Zeichenfolge mit einem '-' habe, erhalte ich einen schlechten Operandentyp für einen unären Fehler.
Simi
Sie können das Negativ einer Zeichenfolge nicht nehmen.
Torsten Marek
Ja, ich weiß, aber so werden die Parameter übergeben. Selbst wenn ich einen Split mache, beginnt der eine oder andere mit '-'. Ich denke, die Sortierschlüssel müssen geteilt werden, bevor key_getter aufgerufen wird. Auf diese Weise überprüft jedes Element in der Schlüsselliste das erste Zeichen. Bin ich auf dem richtigen Weg?
Simi
0

Da Sie bereits mit Lambda vertraut sind, finden Sie hier eine weniger ausführliche Lösung.

>>> def itemgetter(*names):
    return lambda mapping: tuple(-mapping[name[1:]] if name.startswith('-') else mapping[name] for name in names)

>>> itemgetter('a', '-b')({'a': 1, 'b': 2})
(1, -2)
A. Coady
quelle
Das funktioniert nicht. Ich habe: values ​​= ['-Total_Points', 'TOT_PTS_Misc'] dann b als Liste der Diktate Wenn ich g = itemgetter (values) (b) aufrufe, erhalte ich AttributeError: 'list' Objekt hat kein Attribut 'beginwith'
simi
Es wird eine variable Anzahl von Namen verwendet, keine Liste von Namen. Nennen Sie es so: itemgetter (* Werte). Schauen Sie sich den ähnlichen eingebauten operator.itemgetter für ein anderes Beispiel an.
A. Coady