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.