Як сортувати об'єкти за кількома ключами в Python?


97

Або практично, як я можу відсортувати список словників за кількома клавішами?

У мене є список диктовок:

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

і мені потрібно скористатися сортуванням декількох ключів, зворотним Total_Points, а не зворотним TOT_PTS_Misc.

Це можна зробити в командному рядку приблизно так:

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

Але я повинен запустити це через функцію, куди передаю список та клавіші сортування. Наприклад, def multikeysort(dict_list, sortkeys):.

Як можна використовувати лямбда-рядок, який буде сортувати список, для довільної кількості ключів, які передаються у функцію багатоклавішного сортування, та враховувати, що клавіші сортування можуть мати будь-яку кількість ключів, а ті, які потребують зворотного сортування, будуть визначені перед "-" перед ним?

Відповіді:


72

Ця відповідь працює для будь-якого стовпця у словнику - заперечений стовпець не повинен бути числом.

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)

Ви можете назвати це так:

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

Спробуйте, заперечивши будь-який стовпець. Ви побачите порядок сортування зворотним.

Далі: змініть його, щоб він не використовував зайвий клас ....


17.01.2016

Беручи чергове натхнення з цієї відповіді. Який найкращий спосіб отримати перший предмет із ітераційного відповідника умові? , Я скоротив код:

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)

Якщо вам подобається ваш лаконічний код.


Пізніше 2016-01-17

Це працює з python3 (який усунув cmpаргумент 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))

Натхненний цією відповіддю Як мені зробити власне сортування в Python 3?


Це працює найкраще, тому що я можу використовувати реверс на будь-яких клавішах або стовпцях. Дякую!
simi

Отже, це працює добре. Я викликаю свою функцію зі списком і рядком як параметри. Спочатку я розділяю рядок, а потім викликаю мультиклавіш із списком та списком ключів із розділеного рядка. Не має значення, який елемент у рядку має знак "-" на початку назви стовпця, оскільки він буде працювати як з елементом, так і з усіма елементами. Приголомшливо Дякую.
simi

2
Дякую, ти врятував мій день!
Сандер ван Левен

4
cmp()недоступний для Python3, так що я повинен був визначити це сам, як уже згадувалося тут: stackoverflow.com/a/22490617/398514
pferate

8
@hughdbrown: Ви видалили cmpключове слово, але cmp()функція все ще використовується на 4 рядки вище. Я спробував це з 3.2, 3.3, 3.4 та 3.5, вони всі не вдалися під час виклику функції, оскільки cmp()не визначено. Третій маркер тут ( docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons ) згадує про лікування cmp()як про відмову .
пферат

55

Ця стаття має хороший виклад різних методів для цього. Якщо ваші вимоги простіші, ніж "повний двонаправлений багатофункціональний ключ", подивіться. Зрозуміло, що прийнята відповідь і пост у блозі, на який я щойно посилався, якимось чином вплинули один на одного, хоча я не знаю, в якому порядку.

На випадок, якщо посилання загине, ось дуже короткий конспект прикладів, не розглянутих вище:

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

Наскільки я можу зрозуміти, stygianvision використовує мій код і не дає жодного кредиту. Google forresult = cmp(fn(left), fn(right))
hughdbrown

4
Дякую за короткий зміст, Link насправді мертвий. :)
Amyth

49

Я знаю, що це досить старе запитання, але жодна з відповідей не згадує, що Python гарантує стабільний порядок сортування для своїх процедур сортування, таких як list.sort()і sorted(), що означає, що елементи, які порівнюються, зберігають свій початковий порядок.

Це означає, що еквівалент ORDER BY name ASC, age DESC(із використанням нотації SQL) для списку словників можна зробити так:

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

Зверніть увагу, як елементи спочатку сортуються за атрибутом "менший" age(за спаданням), потім за атрибутом "основний" name, що веде до правильного остаточного порядку.

Реверсування / інвертування працює для всіх типів, що можна замовити, а не лише для цифр, які ви можете заперечити, поставивши знак мінус попереду.

І через алгоритм Тимсорта, що використовується в (принаймні) CPython, це насправді досить швидко на практиці.


2
дуже хороша. для помірних наборів даних, де сортування набору кілька разів не має значення, це чудово! Як ви вказуєте, вам доведеться змінити сортування python порівняно з сортуванням sql. Дякую.
Грег

Друге сортування порушить результат першого. Забавно, що ніхто з виборців цього не помітив.
вулкан

9
смішно, що ви не помітили, що основний критерій сортування виконується в останню чергу, як показано в моєму прикладі, і чітко згадані в іншому коментарі, щоб це було дуже чітко, якщо ви цього не помітили.
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']))

Оце Так! Це неймовірно. Це чудово працює. Я такий новачок, що відчуваю, що ніколи не дійду до того, щоб знати все це. Це теж було швидко. Дуже дякую.
simi

Але що, якщо ключі, що надсилаються до sortkeypicker, є рядком, як-от '-Total_Points, TOT_PTS_Misc'?
simi

1
Тоді ви можете спочатку розділити рядок на масив, зателефонувавшиsome_string.split(",")
Джейсон Крейтон

Дякую. Я зрозумів, що можу зробити розщеплення рядка, вже прокоментувавши. DOH!
simi

2
Але що, якщо замінити значення рядка замість значення числа? Я не думаю, що це могло б спрацювати.
Нік Перкінс,

5

Я використовую наступне для сортування 2d-масиву за кількістю стовпців

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

Це може бути розширено для роботи над довільною кількістю елементів. Я вважаю, що пошук кращого шаблону доступу до ваших сортувальних ключів є кращим, ніж написання вигадливого компаратора.

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

У мене була подібна проблема сьогодні - мені довелося сортувати елементи словника за спаданням числових значень та за зростанням рядкових значень. Щоб вирішити проблему суперечливих вказівок, я заперечив цілі значення.

Ось варіант мого рішення - як застосовується до OP

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

Дуже просто - і працює як шарм

[{'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)

Демонстрація:

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

Синтаксичний розбір дещо крихкий, але принаймні він дозволяє змінювати кількість пробілів між клавішами.


Але коли у мене є другий елемент у рядку із знаком "-", це дає мені поганий тип операнда для унарної помилки.
simi

Не можна приймати мінус рядка.
Торстен Марек

Так, я знаю, але саме таким чином передаються параметри. Навіть якщо я роблю поділ, той чи інший розпочнеться з "-". Я думаю, що клавіші сортування потрібно розділити перед викликом key_getter, таким чином кожен елемент у списку ключів перевірить перший символ. Я на правильному шляху?
simi

0

Оскільки вам вже комфортно лямбда, ось ось менш детальне рішення.

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

Це не працює. У мене є: values ​​= ['-Total_Points', 'TOT_PTS_Misc'], потім b як список диктів. Коли я викликаю g = itemgetter (values) (b), я отримую AttributeError: об'єкт "list" не має атрибута "startwith"
simi

Потрібна змінна кількість імен, а не перелік імен. Назвіть це так: itemgetter (* значення). Подивіться на подібний вбудований operator.itemgetter для іншого прикладу.
А.Коді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.