Jak sortować obiekty według wielu kluczy w Pythonie?

97

Lub, praktycznie, jak mogę posortować listę słowników według wielu kluczy?

Mam listę dyktand:

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

i muszę użyć sortowania z wieloma klawiszami odwróconymi przez Total_Points, a następnie nie odwróconych przez TOT_PTS_Misc.

Można to zrobić w wierszu polecenia w następujący sposób:

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

Ale muszę to uruchomić za pomocą funkcji, w której przekazuję listę i klucze sortowania. Na przykład def multikeysort(dict_list, sortkeys):.

W jaki sposób można użyć wiersza lambda, który posortuje listę dla dowolnej liczby kluczy, które są przekazywane do funkcji multikeysort, i wziąć pod uwagę, że sortkeys mogą mieć dowolną liczbę kluczy, a te, które wymagają odwróconych sortowań, zostaną zidentyfikowane z „-” przed nim?

simi
źródło

Odpowiedzi:

72

Ta odpowiedź działa dla każdego rodzaju kolumny w słowniku - kolumna zanegowana nie musi być liczbą.

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)

Możesz to nazwać tak:

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

Spróbuj z zanegowaną dowolną kolumną. Zobaczysz odwróconą kolejność sortowania.

Następnie: zmień go, aby nie używał dodatkowej klasy ....


2016-01-17

Czerpię inspirację z tej odpowiedzi. Jaki jest najlepszy sposób na uzyskanie pierwszego elementu z iterowalnego dopasowania warunku? Skróciłem kod:

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)

Jeśli podoba Ci się zwięzły kod.


Później 17.01.2016

Działa to z pythonem3 (co wyeliminowało cmpargument do 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))

Zainspirowany tą odpowiedzią Jak zrobić sortowanie niestandardowe w Pythonie 3?

hughdbrown
źródło
Działa to najlepiej, ponieważ mogę użyć odwrotności na dowolnych klawiszach lub kolumnach. Dziękuję Ci!
simi
Więc to działa dobrze. Wywołuję swoją funkcję z listą i ciągiem znaków jako parametrami. Najpierw dzielę ciąg, a następnie wywołuję multikeysort z listą i listą kluczy z podzielonego ciągu. Nie ma znaczenia, który element ciągu ma znak „-” na początku nazwy kolumny, ponieważ będzie działać zarówno z elementem, jak i wszystkimi elementami. Niesamowite. Dziękuję Ci.
simi
2
Dzięki, uratowałeś mi dzień!
Sander van Leeuwen
4
cmp()nie jest dostępny dla Pythona3, więc musiałem go zdefiniować samodzielnie, jak wspomniano tutaj: stackoverflow.com/a/22490617/398514
pferate
8
@hughdbrown: Usunąłeś cmpsłowo kluczowe, ale cmp()funkcja jest nadal używana 4 wiersze powyżej. Wypróbowałem z 3.2, 3.3, 3.4 i 3.5, wszystkie zawiodły przy wywołaniu funkcji, ponieważ cmp()nie są zdefiniowane. Trzeci punkt tutaj ( docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons ) wspomina o traktowaniu cmp()jako zniknęło.
nadaj
56

Ten artykuł zawiera niezłe podsumowanie różnych technik, aby to zrobić. Jeśli Twoje wymagania są prostsze niż „pełne dwukierunkowe wielokierunkowe”, przyjrzyj się. Jest jasne, że zaakceptowana odpowiedź i wpis na blogu, do którego właśnie się odwołałem, wpłynęły na siebie w jakiś sposób, chociaż nie wiem, w jakiej kolejności.

W przypadku, gdy link zniknie, oto bardzo krótkie streszczenie przykładów nieuwzględnionych powyżej:

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
źródło
O ile wiem, stygianvision używa mojego kodu i nie podaje kredytu. Google forresult = cmp(fn(left), fn(right))
hughdbrown,
4
Dzięki za streszczenie, Link nie żyje. :)
Amyth
49

Wiem, że jest to dość stare pytanie, ale żadna z odpowiedzi nie wspomina, że ​​Python gwarantuje stabilny porządek sortowania dla swoich procedur sortowania, takich jak list.sort()i sorted(), co oznacza, że ​​elementy, które porównują równe, zachowują swoją pierwotną kolejność.

Oznacza to, że odpowiednik ORDER BY name ASC, age DESC(przy użyciu notacji SQL) dla listy słowników można zrobić w następujący sposób:

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

Zwróć uwagę, jak pozycje są najpierw sortowane według atrybutu „mniejszy” age(malejąco), a następnie według atrybutu „major” name, co prowadzi do właściwej ostatecznej kolejności.

Odwrócenie / odwrócenie działa dla wszystkich możliwych do uporządkowania typów, a nie tylko liczb, które można zanegować, umieszczając na początku znak minus.

A ze względu na algorytm Timsort używany w (przynajmniej) CPythonie, w praktyce jest to dość szybkie.

wouter bolsterlee
źródło
2
bardzo dobrze. w przypadku średnich zestawów danych, w których wielokrotne sortowanie zestawu nie ma znaczenia, jest to super fajne! Jak zauważyłeś, musisz odwrócić sortowanie w Pythonie w porównaniu z sortowaniem sql. Dzięki.
Greg
Drugi rodzaj przełamie wynik pierwszego. Zabawne, że żaden z popierających tego nie zauważył.
wulkan
9
zabawne, że nie zauważyłeś, że podstawowe kryterium sortowania jest ostatnie, jak pokazano w moim przykładzie, i zostało wyraźnie wspomniane w innym komentarzu, aby było bardzo jasne, gdybyś tego nie zauważył.
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
źródło
Łał! To jest niesamowite. Działa świetnie. Jestem takim nowicjuszem, że czuję, że nigdy nie dojdę do tego stopnia, że ​​będę wiedział to wszystko. To też było szybkie. Dziękuję Ci bardzo.
simi
Ale co, jeśli klucze wysyłane do sortkeypicker są ciągiem znaków, na przykład „-Total_Points, TOT_PTS_Misc”?
simi
1
Następnie możesz najpierw podzielić ciąg na tablicę, wywołującsome_string.split(",")
Jason Creighton
Dziękuję Ci. Zdałem sobie sprawę, że mogę zrobić podział łańcucha, po tym, jak już skomentowałem. DOH!
simi
2
Ale co, jeśli zanegujesz wartość ciągu zamiast wartości liczbowej? Myślę, że to nie zadziała.
Nick Perkins
5

Używam poniższego do sortowania tablicy 2d na wielu kolumnach

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

Można to rozszerzyć na dowolną liczbę pozycji. Wydaje mi się, że znalezienie lepszego wzoru dostępu do kluczy, które można sortować, jest lepsze niż pisanie fantazyjnego komparatora.

>>> 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
źródło
4

Miałem dziś podobny problem - musiałem sortować elementy słownika według malejących wartości liczbowych i rosnących wartości ciągów. Aby rozwiązać problem sprzecznych kierunków, zanegowałem wartości całkowite.

Oto wariant mojego rozwiązania - w przypadku OP

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

Bardzo proste - i działa jak urok

[{'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}]
wulkan
źródło
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)

Demonstracja:

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

Parsowanie jest nieco kruche, ale przynajmniej pozwala na zmienną liczbę spacji między kluczami.

Torsten Marek
źródło
Ale kiedy mam drugi element w ciągu z „-”, daje mi to zły typ operandu dla jednoargumentowego - błąd.
simi
Nie możesz wziąć ujemnej strony łańcucha.
Torsten Marek
Tak, wiem, ale tak są przekazywane parametry. Nawet jeśli zrobię podział, jeden lub drugi będzie zaczynał się od „-”. Myślę, że sortkeys należy podzielić przed wywołaniem key_getter, w ten sposób każdy element na liście kluczy sprawdzi pierwszy znak. Czy jestem na dobrej drodze?
simi
0

Ponieważ już dobrze znasz lambdę, oto mniej rozwlekłe rozwiązanie.

>>> 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
źródło
To nie działa. Mam: values ​​= ['-Total_Points', 'TOT_PTS_Misc'], a następnie b jako listę dykt. Kiedy dzwonię do g = itemgetter (values) (b) otrzymuję AttributeError: obiekt 'list' nie ma atrybutu 'startedwith'
simi
Wymaga zmiennej liczby nazwisk, a nie listy nazwisk. Nazwij to tak: itemgetter (* wartości). Spójrz na podobny wbudowany operator.itemgetter, aby uzyskać inny przykład.
A. Coady