Как сортировать объекты по нескольким ключам в 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):.

Как можно использовать лямбда-строку, которая будет сортировать список для произвольного количества ключей, которые передаются в функцию множественной сортировки, и принять во внимание, что ключи сортировки могут иметь любое количество ключей, а те, которые нуждаются в обратной сортировке, будут идентифицированы с '-' перед ним?

сими
источник

Ответы:

73

Этот ответ работает для любого столбца в словаре - столбец с отрицанием не обязательно должен быть числом.

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

Попробуйте это с отрицанием любого столбца. Вы увидите обратный порядок сортировки.

Далее: измените его так, чтобы он не использовал дополнительный класс ....


2016-01-17

Вдохновляясь этим ответом, как лучше всего получить первый элемент из итерации, соответствующей условию? , Я сократил код:

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
Спасибо, ты спас мне день!
Sander van Leeuwen
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()как пропавших .
pferate 04
57

В этой статье есть хорошее изложение различных методов для этого. Если ваши требования проще, чем «полная двунаправленная мультиклавиша», обратите внимание. Ясно, что принятый ответ и сообщение в блоге, на которое я только что ссылался, каким-то образом повлияли друг на друга, хотя я не знаю, в каком порядке.

Если ссылка не работает, вот очень краткий обзор примеров, не описанных выше:

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 дляresult = cmp(fn(left), fn(right))
hughdbrown
4
Спасибо за синопсис, Линк уже мертв. :)
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, что приводит к правильному окончательному порядку.

Реверс / инвертирование работает для всех заказываемых типов, а не только для чисел, которые вы можете отменить, поставив знак минус впереди.

И из-за алгоритма Timsort, используемого (по крайней мере) в CPython, на практике это довольно быстро.

Wouter Bolsterlee
источник
2
очень хорошо. для умеренных наборов данных, где сортировка набора несколько раз не имеет значения, это супер круто! Как вы указываете, вам нужно отменить сортировку python по сравнению с сортировкой sql. Спасибо.
Грег,
Второй вид нарушит результат первого. Забавно, что никто из сторонников этого не заметил.
вулкан
9
забавно, что вы не заметили, что первичный критерий сортировки идет последним, как показано в моем примере, и явно упомянут в другом комментарии, чтобы прояснить это на случай, если вы не заметили.
wouter bolsterlee 06
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 как список dicts. Когда я вызываю g = itemgetter (values) (b), я получаю AttributeError: объект 'list' не имеет атрибута 'startwith'
simi
Требуется переменное количество имен, а не список имен. Назовите это так: itemgetter (* значения). Взгляните на аналогичный встроенный operator.itemgetter для другого примера.
A. Coady