2009-07-17 29 views
69

Mam listę dicts:Python sortowanie listy słowników przez wielu kluczy

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ę korzystać z multi klucz sortowania odwrócony przez Total_Points, wtedy nie odwrócony przez TOT_PTS_Misc.

Można to zrobić w wierszu polecenia tak:

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

Ale muszę uruchomić to przez funkcję, gdzie przechodzą na listy i klucze sortowania. Na przykład: def multikeysort(dict_list, sortkeys):.

W jaki sposób można użyć linii lambda, która sortuje listę, dla dowolnej liczby kluczy, które są przekazywane do funkcji multikeysort, i weź pod uwagę, że klucze sortujące mogą mieć dowolną liczbę kluczy i te, które wymagają odwrócenia rodzaje będą identyfikowane za pomocą "-" przed nim?

Odpowiedz

61

Ta odpowiedź działa dla każdego rodzaju kolumny w słowniku - zanegowana kolumna 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żna 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 jednej kolumny do pozbawienia. Zobaczysz odwrócenie porządku sortowania.

Następny: zmienić go tak, aby nie używać dodatkowego klasę ....


2016-01-17

Biorąc inspirację z tej odpowiedzi What is the best way to get the first item from an iterable matching a condition?, ja skrócony 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ę twój kod zwięzły.


Później 2016-01-17

Działa z python3 (co eliminowało cmp argument sort):

from operator import itemgetter as i 
from functools import cmp_to_key 

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ą odpowiedź How should I do custom sort in Python 3?

+0

Działa to najlepiej, ponieważ mogę korzystać z rewersu na dowolnych klawiszach lub kolumnach. Dziękuję Ci! – simi

+0

To działa dobrze. Nazwam moją funkcję z listą i ciągiem jako parametrami. Najpierw podzielę ciąg, a następnie wywołaj multikeysort z listą i listą kluczy z podzielonego ciągu. Nie ma znaczenia, który element w łańcuchu ma "-" na początku nazwy kolumny, ponieważ będzie działał zarówno z elementem, jak i wszystkimi elementami. Niesamowite. Dziękuję Ci. – simi

+0

Załóżmy, że na liście dyktów (b) znajdują się elementy, które nie mają kluczy, którymi chciałbym sortować. Jak mógłbym dla nich przetestować? Próbowałem spróbować/oprócz, ale to nic nie zwraca. – simi

22
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'])) 
+0

Wow! To jest niesamowite. Działa świetnie. Jestem nowicjuszem, który wydaje mi się, że nigdy nie dojdę do tego, by o tym wszystkim wiedzieć. To też było szybkie. Dziękuję Ci bardzo. – simi

+0

Ale co się stanie, jeśli klucze wysłane do sortkeypicker to ciąg znaków, jak np. "-Total_Points, TOT_PTS_Misc"? – simi

+1

Następnie możesz najpierw podzielić ciąg na tablicę, wywołując 'some_string.split (", ")' –

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ą odstępów między klawiszami.

+0

Ale kiedy mam drugi element w ciągu znaków z "-", to daje mi zły typ argumentu dla unary - błąd. – simi

+0

Nie można przyjąć ujemnego ciągu znaków. –

+0

Tak, wiem, ale w ten sposób parametry są przekazywane. Nawet jeśli zrobię podział, jeden lub drugi zacznie się od "-". Myślę, że sortkeys muszą zostać podzielone przed wywołaniem key_getter, w ten sposób każdy element na liście kluczy będzie sprawdzał pierwszy znak. Czy jestem na dobrej drodze? – simi

0

skoro” już wygodne z lambda, oto mniej szczegółowe 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) 
+0

To nie działa. Mam: wartości = ['-Total_Points', 'TOT_PTS_Misc'] następnie b jako lista dyktafonów Gdy wywołuję g = itemgetter (wartości) (b) otrzymuję AttributeError: 'list' obiekt nie ma atrybutu 'startswith ' – simi

+0

Wymaga zmiennej liczby nazw, a nie listy nazw. Nazwij to tak: itemgetter (* wartości). Spójrz na podobny wbudowany operator.itemgetter dla innego przykładu. –

5

używam następujących do sortowania tablicy 2D na kilku kolumnach

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

ten może być przedłużony do pracy na dowolnej liczby elementów. Myślę, że znalezienie lepszego wzoru dostępu do sortowalnych kluczy jest lepsze niż napisanie odpowiedniego 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]] 
21

http://stygianvision.net/updates/python-sort-list-object-dictionary-multiple-key/ ma dobry przegląd różnych technik. Jeśli twoje wymagania są prostsze niż "pełny dwukierunkowy multikolor", spójrz. Oczywiste jest, że zaakceptowana odpowiedź i wpis na blogu, o którym wspomniałem, wpłynęły w jakiś sposób na siebie nawzajem, chociaż nie wiem, która to kolejność.

W przypadku, gdy związek umiera tutaj jest bardzo szybkie streszczenie przykładów nieobjęte 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'])) 
+0

Tak blisko, jak mogę powiedzieć, stygianvision używa mojego kodu i nie daje kredytu. Google dla 'result = cmp (fn (po lewej), fn (po prawej))' – hughdbrown

+1

Dzięki za streszczenie, Link jest obecnie martwy. :) – Amyth

18

wiem, że to raczej stare pytanie, ale żadna z odpowiedzi wspomnieć, że Python gwarantuje stabilny porządek sortowania jego procedury sortowania, takie jak list.sort() i sorted(), co oznacza, że ​​przedmioty, które są porównywane, zachowują swoją oryginalną kolejność.

Oznacza to, że równowartość ORDER BY name ASC, age DESC (z użyciem notacji SQL) na liście słowników można zrobić tak:

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

cofania/odwracanie działa dla wszystkich zamawianych rodzajów, nie tylko liczby, które można zanegować, umieszczając znak minus z przodu.

A z powodu algorytmu Timsorta używanego w (co najmniej) CPythonie, jest to w praktyce dość szybkie.

+1

bardzo ładne. dla umiarkowanych zestawów danych, w których sortowanie zestawu wiele razy nie ma znaczenia, to jest super fajne! Jak podkreślasz, musisz odwrócić sortowanie python w porównaniu do sortowania sql. Dzięki. – Greg

Powiązane problemy