Сортировать список по нескольким атрибутам?

457

У меня есть список списков:

[[12, 'tall', 'blue', 1],
[2, 'short', 'red', 9],
[4, 'tall', 'blue', 13]]

Если бы я хотел отсортировать по одному элементу, скажем, высокому / короткому элементу, я мог бы сделать это с помощью s = sorted(s, key = itemgetter(1)).

Если бы я захотел отсортировать как по высокому / короткому, так и по цвету, я мог бы выполнить сортировку дважды, по одному разу для каждого элемента, но есть ли более быстрый способ?

Головная боль
источник
8
Если вы используете кортежи вместо списков, python упорядочивает сортировку по записям слева направо при запуске sort. То есть sorted([(4, 2), (0, 3), (0, 1)]) == [(0, 1), (0, 3), (4, 2)].
Матин Улхак,

Ответы:

772

Ключ может быть функцией, которая возвращает кортеж:

s = sorted(s, key = lambda x: (x[1], x[2]))

Или вы можете добиться того же, используя itemgetter(что быстрее и позволяет избежать вызова функции Python):

import operator
s = sorted(s, key = operator.itemgetter(1, 2))

И обратите внимание, что здесь вы можете использовать sortвместо использования, sortedа затем переназначить:

s.sort(key = operator.itemgetter(1, 2))
Марк Байерс
источник
20
Для полноты из таймита: для меня первый дал 6 нас за цикл, а второй 4,4 нас за цикл
Брайан Ларсен
10
Есть ли способ сортировки первого по возрастанию, а второго по убыванию? (Предположим, что оба атрибута являются строками, поэтому без хаков, как добавление -целых чисел)
Мартин Тома
73
как насчет того, если я хочу подать заявку revrse=Trueтолько на x[1]это возможно?
Амит
28
@moose, @Amyth, чтобы вернуться только к одному атрибуту, вы можете отсортировать дважды: сначала по вторичному, а s = sorted(s, key = operator.itemgetter(2))затем по первичному. s = sorted(s, key = operator.itemgetter(1), reverse=True)Не идеально, но работает.
Tomcounsell
52
@ Амит или другая опция, если ключ является числом, чтобы сделать его обратным, вы можете умножить его на -1.
Серж
37

Я не уверен, что это самый питонический метод ... У меня был список кортежей, которым нужно было отсортировать 1-е место по убыванию целочисленных значений и 2-е по алфавиту. Это потребовало реверсирования целочисленной сортировки, но не алфавитной сортировки. Вот мое решение: (кстати, на лету на экзамене, я даже не знал, что вы можете «вкладывать» отсортированные функции)

a = [('Al', 2),('Bill', 1),('Carol', 2), ('Abel', 3), ('Zeke', 2), ('Chris', 1)]  
b = sorted(sorted(a, key = lambda x : x[0]), key = lambda x : x[1], reverse = True)  
print(b)  
[('Abel', 3), ('Al', 2), ('Carol', 2), ('Zeke', 2), ('Bill', 1), ('Chris', 1)]
Клинт Блатчфорд
источник
13
поскольку 2nd - это число, оно работает так, как показано на примере, b = sorted(a, key = lambda x: (-x[1], x[0]))который более очевиден в отношении критериев, применяемых в первую очередь. Что касается эффективности, я не уверен, кому-то нужно время.
Андрей-Никулае Петре
5

Несколько лет опоздал на вечеринку, но я хочу как отсортировать по двум критериям, так и использовать reverse=True. Если кто-то еще хочет знать, как, вы можете заключить свои критерии (функции) в круглые скобки:

s = sorted(my_list, key=lambda i: ( criteria_1(i), criteria_2(i) ), reverse=True)
donrondadon
источник
5

Похоже, вы могли бы использовать listвместо tuple. Это становится более важным, я думаю, когда вы захватываете атрибуты вместо «магических индексов» списка / кортежа.

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

Итак, сначала я определил вспомогательный метод

def attr_sort(self, attrs=['someAttributeString']:
  '''helper to sort by the attributes named by strings of attrs in order'''
  return lambda k: [ getattr(k, attr) for attr in attrs ]

затем использовать его

# would defined elsewhere but showing here for consiseness
self.SortListA = ['attrA', 'attrB']
self.SortListB = ['attrC', 'attrA']
records = .... #list of my objects to sort
records.sort(key=self.attr_sort(attrs=self.SortListA))
# perhaps later nearby or in another function
more_records = .... #another list
more_records.sort(key=self.attr_sort(attrs=self.SortListB))

Это будет использовать сгенерированную лямбда-функцию для сортировки списка, object.attrAа затем object.attrBпри условии, что objectесть получатель, соответствующий предоставленным именам строк. И второй случай будет сортировать к тому object.attrCвремениobject.attrA .

Это также позволяет вам потенциально представлять варианты внешней сортировки для совместного использования потребителем, модульным тестом или для них, чтобы, возможно, сказать вам, как они хотят, чтобы сортировка выполнялась для какой-либо операции в вашем API, только для того, чтобы дать вам список, а не связывая их с вашей серверной реализацией.

UpAndAdam
источник
Хорошо сделано. Что делать, если атрибуты должны быть отсортированы в разных порядках? Предположим, что attrA сортируется по возрастанию, а attrB по убыванию? Есть ли быстрое решение поверх этого? Спасибо!
mhn_namak
1

Вот один из способов: вы в основном переписываете свою функцию сортировки, чтобы получить список функций сортировки, каждая функция сортировки сравнивает атрибуты, которые вы хотите протестировать, при каждом тесте сортировки вы смотрите и видите, возвращает ли функция cmp ненулевой возврат если так, сломайте и отправьте возвращаемое значение. Вы вызываете это, вызывая лямбду-функцию из списка лямбда-выражений.

Его преимущество заключается в том, что он выполняет однократный проход через данные, а не в виде предыдущего вида, как это делают другие методы. Другое дело, что сортировка происходит на месте, тогда как сортировка, похоже, делает копию.

Я использовал его для написания функции ранжирования, которая ранжирует список классов, где каждый объект находится в группе и имеет функцию оценки, но вы можете добавить любой список атрибутов. Обратите внимание на не лямбда-подобное, хотя и хакерское использование лямбды для вызова сеттера. Часть ранга не будет работать для массива списков, но сортировка будет.

#First, here's  a pure list version
my_sortLambdaLst = [lambda x,y:cmp(x[0], y[0]), lambda x,y:cmp(x[1], y[1])]
def multi_attribute_sort(x,y):
    r = 0
    for l in my_sortLambdaLst:
        r = l(x,y)
        if r!=0: return r #keep looping till you see a difference
    return r

Lst = [(4, 2.0), (4, 0.01), (4, 0.9), (4, 0.999),(4, 0.2), (1, 2.0), (1, 0.01), (1, 0.9), (1, 0.999), (1, 0.2) ]
Lst.sort(lambda x,y:multi_attribute_sort(x,y)) #The Lambda of the Lambda
for rec in Lst: print str(rec)

Вот способ ранжировать список объектов

class probe:
    def __init__(self, group, score):
        self.group = group
        self.score = score
        self.rank =-1
    def set_rank(self, r):
        self.rank = r
    def __str__(self):
        return '\t'.join([str(self.group), str(self.score), str(self.rank)]) 


def RankLst(inLst, group_lambda= lambda x:x.group, sortLambdaLst = [lambda x,y:cmp(x.group, y.group), lambda x,y:cmp(x.score, y.score)], SetRank_Lambda = lambda x, rank:x.set_rank(rank)):
    #Inner function is the only way (I could think of) to pass the sortLambdaLst into a sort function
    def multi_attribute_sort(x,y):
        r = 0
        for l in sortLambdaLst:
            r = l(x,y)
            if r!=0: return r #keep looping till you see a difference
        return r

    inLst.sort(lambda x,y:multi_attribute_sort(x,y))
    #Now Rank your probes
    rank = 0
    last_group = group_lambda(inLst[0])
    for i in range(len(inLst)):
        rec = inLst[i]
        group = group_lambda(rec)
        if last_group == group: 
            rank+=1
        else:
            rank=1
            last_group = group
        SetRank_Lambda(inLst[i], rank) #This is pure evil!! The lambda purists are gnashing their teeth

Lst = [probe(4, 2.0), probe(4, 0.01), probe(4, 0.9), probe(4, 0.999), probe(4, 0.2), probe(1, 2.0), probe(1, 0.01), probe(1, 0.9), probe(1, 0.999), probe(1, 0.2) ]

RankLst(Lst, group_lambda= lambda x:x.group, sortLambdaLst = [lambda x,y:cmp(x.group, y.group), lambda x,y:cmp(x.score, y.score)], SetRank_Lambda = lambda x, rank:x.set_rank(rank))
print '\t'.join(['group', 'score', 'rank']) 
for r in Lst: print r
Доминик Сучу
источник