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?

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?


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

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.


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

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

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

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.


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)

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.