Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Sort 2 Element Tuple of Strings in Mixed Order Using key Parameter (Not cmp)

In Python, I have something like the following (although randomly shuffled):

l = [('a', 'x'),
     ('a', 'y'),
     ('a', 'z'),
     ('b', 'x'),
     ('b', 'y'),
     ('b', 'z'),
    ]

If I call sorted(l), I get a sorted result (like above) which is what one would expect. However, what I need is to forward sort on the first element of the tuple and reverse sort on the second element. In other words, I would like the following result:

l = [('a', 'z'),
     ('a', 'y'),
     ('a', 'x'),
     ('b', 'z'),
     ('b', 'y'),
     ('b', 'x'),
    ]

In Python2.x, there exists a cmp parameter that can be passed to sorted() to achieve this result, but Python3 no longer has this. It has only a key parameter. Is there any way to achieve the desired sort order using only the key parameter?

I know I can define a whole new class to wrap my tuples, or use something like functools.cmp_to_key (which also creates a wrapper class), but that all seems heavy handed for such a simple operation. Is there no other recourse?

edit: I should add that the strings won't all be one-character strings and that, in some cases, the list of tuples contain non-string data [i.e. (basestring, datetime)].

like image 235
dave mankoff Avatar asked Feb 03 '12 18:02

dave mankoff


4 Answers

In a single step:

>>> l.sort(key=lambda t: (t[0], -ord(t[1])))
>>> l
[('a', 'z'), ('a', 'y'), ('a', 'x'), ('b', 'z'), ('b', 'y'), ('b', 'x')]

Any time you need to sort on multiple keys you can make your key function a tuple since tuples are compared lexicographically. If you need to reverse sort on one of the keys just make that element negative. Obviously you can't just make the string negative so you first need to convert it to an integer with ord.

like image 34
Andrew Clark Avatar answered Oct 02 '22 20:10

Andrew Clark


The Python Sorting HOWTO recommends that you take advantage of sort stability and do the sort in two passes:

>>> l.sort(key=lambda t: t[1], reverse=True)   # SECONDARY KEY: field 1 descending
>>> l.sort(key=lambda t: t[0])                 # PRIMARY KEY:   field 0 ascending
>>> l
[('a', 'z'), ('a', 'y'), ('a', 'x'), ('b', 'z'), ('b', 'y'), ('b', 'x')]
like image 33
Raymond Hettinger Avatar answered Oct 02 '22 20:10

Raymond Hettinger


Sorting in Python is stable as of Python 2.2.

So, you can sort by the second value first with the reverse flag on:

>>> from operator import itemgetter
>>> l.sort(key=itemgetter(1), reverse=True)
>>> l
[('a', 'z'), ('b', 'z'), ('a', 'y'), ('b', 'y'), ('a', 'x'), ('b', 'x')]

Then you can sort by the first value:

>>> l.sort(key=itemgetter(0))
>>> l
[('a', 'z'), ('a', 'y'), ('a', 'x'), ('b', 'z'), ('b', 'y'), ('b', 'x')]
like image 64
ovgolovin Avatar answered Oct 02 '22 19:10

ovgolovin


You could accomplish this by running sorted twice on the list, first using a reverse sort with the second item in each tuple, then using a normal sort on the newly sorted list with the first item in each tuple.

l = [('a', 'y'),
     ('a', 'x'),
     ('b', 'y'),
     ('b', 'x'),
     ('b', 'z'),
     ('a', 'z'),
    ]
l = sorted(l, key=lambda tup: tup[1], reverse=True)
l = sorted(l, key=lambda tup: tup[0])

Or, if you prefer:

l = sorted(sorted(l, key=lambda tup: tup[1], reverse=True),
           key=lambda tup: tup[0])
like image 40
clu_ Avatar answered Oct 02 '22 20:10

clu_