Comment trier les objets par plusieurs clés en Python?

96

Ou, en pratique, comment puis-je trier une liste de dictionnaires par plusieurs touches?

J'ai une liste de dictés:

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}]

et je dois utiliser un tri multi-clé inversé par Total_Points, puis non inversé par TOT_PTS_Misc.

Cela peut être fait à l'invite de commande comme ceci:

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

Mais je dois exécuter cela via une fonction, où je passe dans la liste et les clés de tri. Par exemple def multikeysort(dict_list, sortkeys):,.

Comment la ligne lambda peut-elle être utilisée pour trier la liste, pour un nombre arbitraire de clés qui sont passées à la fonction multikeysort, et prendre en compte que les clés de tri peuvent avoir n'importe quel nombre de clés et que celles qui nécessitent des tris inversés seront identifiées avec un '-' avant?

simi
la source

Réponses:

72

Cette réponse fonctionne pour tout type de colonne dans le dictionnaire - la colonne inversée n'a pas besoin d'être un nombre.

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)

Vous pouvez l'appeler comme ceci:

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

Essayez-le avec l'une ou l'autre des colonnes annulée. Vous verrez l'ordre de tri inversé.

Suivant: changez-le pour qu'il n'utilise pas de classe supplémentaire ...


17/01/2016

Je m'inspire de cette réponse Quelle est la meilleure façon d'obtenir le premier élément d'un itérable correspondant à une condition? , J'ai raccourci le code:

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)

Au cas où vous aimez votre code laconique.


Plus tard 17/01/2016

Cela fonctionne avec python3 (qui a éliminé l' cmpargument de 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))

Inspiré par cette réponse Comment faire un tri personnalisé dans Python 3?

hughdbrown
la source
Cela fonctionne le mieux car je peux utiliser l'inverse sur toutes les clés ou colonnes. Je vous remercie!
simi
Cela fonctionne donc bien. J'appelle ma fonction avec la liste et la chaîne comme paramètres. J'ai d'abord divisé la chaîne, puis j'appelle le tri multiple avec la liste et la liste des clés de la chaîne divisée. Peu importe quel élément de la chaîne a le «-» au début du nom de la colonne, car il fonctionnera avec l'élément ou tous les éléments. Impressionnant. Je vous remercie.
simi
2
Merci, tu as sauvé ma journée!
Sander van Leeuwen
4
cmp()n'est pas disponible pour Python3, j'ai donc dû le définir moi-même, comme mentionné ici: stackoverflow.com/a/22490617/398514
pferate
8
@hughdbrown: Vous avez supprimé le cmpmot - clé, mais la cmp()fonction est toujours utilisée 4 lignes ci-dessus. Je l'ai essayé avec 3.2, 3.3, 3.4 et 3.5, ils ont tous échoué à l'appel de fonction, car il cmp()n'est pas défini. La troisième puce ici ( docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons ) mentionne le traitement cmp()comme disparu.
pferate le
53

Cet article présente un bon aperçu des différentes techniques pour ce faire. Si vos exigences sont plus simples que le "multikey bidirectionnel complet", jetez un œil. Il est clair que la réponse acceptée et le billet de blog que je viens de citer se sont influencés d'une certaine manière, même si je ne sais pas dans quel ordre.

Au cas où le lien meurt, voici un résumé très rapide des exemples non couverts ci-dessus:

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
la source
Autant que je sache, stygianvision utilise mon code et ne donne aucun crédit. Google pourresult = cmp(fn(left), fn(right))
hughdbrown
4
Merci pour le synopsis, Link est en fait mort maintenant. :)
Amyth
47

Je sais que c'est une question assez ancienne, mais aucune des réponses ne mentionne que Python garantit un ordre de tri stable pour ses routines de tri telles que list.sort()et sorted(), ce qui signifie que les éléments qui se comparent égaux conservent leur ordre d'origine.

Cela signifie que l'équivalent de ORDER BY name ASC, age DESC(en utilisant la notation SQL) pour une liste de dictionnaires peut être fait comme ceci:

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

Notez comment les éléments sont d'abord triés par l'attribut "moindre" age(décroissant), puis par l'attribut "majeur" name, conduisant à l'ordre final correct.

L'inversion / inversion fonctionne pour tous les types pouvant être commandés, pas seulement pour les nombres que vous pouvez annuler en mettant un signe moins devant.

Et en raison de l'algorithme de Timsort utilisé dans (au moins) CPython, c'est en fait plutôt rapide dans la pratique.

wouter bolsterlee
la source
2
très agréable. pour les ensembles de données modérés où le tri de l'ensemble plusieurs fois n'a pas d'importance, c'est super cool! Comme vous le faites remarquer, vous devez inverser le tri python par rapport au tri SQL. Merci.
Greg
Le second tri cassera le résultat du premier. C'est drôle qu'aucun des votants positifs ne l'ait remarqué.
volcan
9
drôle que vous n'ayez pas remarqué que le critère de tri principal passe en dernier, comme indiqué dans mon exemple, et explicitement mentionné dans l'autre commentaire pour que ce soit très clair au cas où vous ne l'auriez pas remarqué.
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
la source
Hou la la! C'est génial. Cela fonctionne très bien. Je suis tellement novice que je sens que je n'arriverai jamais à savoir tout cela. C'était rapide aussi. Merci beaucoup.
simi
Mais que se passe-t-il si les clés envoyées au sortkeypicker sont une chaîne, comme "-Total_Points, TOT_PTS_Misc"?
simi
1
Ensuite, vous pouvez d'abord diviser la chaîne en un tableau en appelantsome_string.split(",")
Jason Creighton
Je vous remercie. J'ai réalisé que je pouvais faire un split de la chaîne, après avoir déjà commenté. DOH!
simi
2
Mais que faire si vous annulez la valeur de la chaîne au lieu de la valeur numérique? Je ne pense pas que cela fonctionnerait.
Nick Perkins
5

J'utilise ce qui suit pour trier un tableau 2d sur un certain nombre de colonnes

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

Cela pourrait être étendu pour fonctionner sur un nombre arbitraire d'éléments. J'ai tendance à penser qu'il vaut mieux trouver un meilleur modèle d'accès à vos clés triables que d'écrire un comparateur sophistiqué.

>>> 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
la source
2

J'ai eu un problème similaire aujourd'hui - je devais trier les éléments du dictionnaire par valeurs numériques décroissantes et par valeurs de chaîne ascendantes. Pour résoudre le problème des directions contradictoires, j'ai annulé les valeurs entières.

Voici une variante de ma solution - applicable à OP

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

Très simple - et fonctionne comme un charme

[{'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}]
volcan
la source
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)

Manifestation:

>>> 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'}]

L'analyse est un peu fragile, mais au moins elle permet un nombre variable d'espaces entre les clés.

Torsten Marek
la source
Mais, quand j'ai le deuxième élément dans la chaîne avec un «-», cela me donne un mauvais type d'opérande pour une erreur unaire.
simi
Vous ne pouvez pas prendre le négatif d'une chaîne.
Torsten Marek
Oui, je sais, mais c'est ainsi que les paramètres sont transmis. Même si je fais un split, l'un ou l'autre commencera par «-». Je pense que les clés de tri doivent être séparées avant d'appeler key_getter, de cette façon, chaque élément de la liste des clés vérifiera le premier caractère. Suis-je sur la bonne voie?
simi
0

Puisque vous êtes déjà à l'aise avec lambda, voici une solution moins verbeuse.

>>> 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
la source
Cela ne fonctionne pas. J'ai: values ​​= ['-Total_Points', 'TOT_PTS_Misc'] puis b comme liste de dictionnaires Quand j'appelle g = itemgetter (values) (b) J'obtiens AttributeError: l'objet 'list' n'a pas d'attribut 'startswith'
simi
Il faut un nombre variable de noms, pas une liste de noms. Appelez-le comme ceci: itemgetter (* values). Jetez un œil à l'operator.itemgetter intégré similaire pour un autre exemple.
A. Coady