0

In SQL, we can specify desc/asc order for each sorting key, i.e. order by key1 desc, key2 asc. How can I acheive the same thing to sort a list of tuples in python?

Suppose I have a list like

[("a", "b", "e"), ("b", "a", "c"), ("b", "a", "d")]

I want to use the second key in asc order and the third ky in desc order. So the result is

[("b", "a", "d"), ("b", "a", "c"), ("a", "b", "e")]

Note, the elements can be any type that has comparison methods (i.e. __lt__, __gt__ etc)

Du Shiqiao
  • 377
  • 1
  • 9
  • 1
    You can try with this, `sorted(sorted(l, key=lambda x:x[1]), key=lambda x:x[2], reverse=True)`. There may be a better solution than this! – shaik moeed Feb 27 '20 at 07:12
  • This might help. https://stackoverflow.com/questions/3121979/how-to-sort-list-tuple-of-lists-tuples-by-the-element-at-a-given-index – gmwill934 Feb 27 '20 at 07:12
  • @shaikmoeed There's indeed a better solution than that, namely one that does it correctly :-P – Kelly Bundy Feb 27 '20 at 16:18
  • @HeapOverflow I felt using `sort` for two times is redundant. There may be a better and generic solution than this ;-) !!! – shaik moeed Feb 28 '20 at 04:48
  • @shailmoeed Thank you. At first I didn't understand your solution but indeed it's essentially same as Heap Overflow wrote in his answer. – Du Shiqiao Feb 28 '20 at 05:08
  • @shaikmoeed Well, like I said in my answer, it's pretty efficient. And the code is fairly short as well. And it's what Python's HOW TO suggests. So I think it's the best, unless the values are numbers, in which case I usually do use the negation trick. To be clear, yours produces a wrong order because you sort by highest priority criterion first instead of last. That's an advantage of the negation trick, where applicable: You don't need to reverse the criterion order. – Kelly Bundy Feb 28 '20 at 11:15
  • 1
    @HeapOverflow As I said before, this solution is redundant(as it is not scalable when tuple consists of more elements to sort in different orders). Yes, this can be the best one for this specific post, but I haven't tested the timings though. – shaik moeed Feb 28 '20 at 12:20
  • @shaikmoeed Right, it gets less efficient with more criteria. – Kelly Bundy Feb 28 '20 at 12:45

3 Answers3

3

You need to build a suitable key function for sorting - see Sort tuples based on second parameter and plenty of others

data = [("a", "b", "c"), ("b", "a", "c"), ("b", "a", "d")]


s = sorted(data, key = lambda x: (x[1],-ord(x[2]))) # "trick" to sort descending

print (s)

gives

[('b', 'a', 'd'), ('b', 'a', 'c'), ('a', 'b', 'c')]

You use a "customized" tuple as key and need to be a bit creative building it.

The ord funciton gives you the ascii value of the character, by negating it you "sort descending" on it.

If you sort more then letters you need to be even more creative.


Not seriously tested:

data = [("a", "b", "cesa"), ("b", "a", "ceta"), ("b", "a", "derived")]
s = sorted(data, key = lambda x: (x[1], tuple((-ord(t) for t in x[2])) ) )

to get

[('b', 'a', 'derived'), ('b', 'a', 'ceta'), ('a', 'b', 'cesa')]
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • @patric-artner Thank you for answer. So what if the elements are more than two letters? I can't use ord function in this case. – Du Shiqiao Feb 27 '20 at 07:20
  • @shi you still can, but you need to do it for all letters - no need to adjust x[1] in that case in a similar matter because it sorts ascending fine – Patrick Artner Feb 27 '20 at 07:21
  • Thanks again. I updated my question. In fact I wanted to ask a more general case, not only string or numbers. I want to tweak sorting order for any comparable objects. – Du Shiqiao Feb 27 '20 at 07:27
1

I found a way where I create a sorter class that has custom comparison methods. This is not an efficient way but works fine.

from functools import total_ordering


def _apply(v, k):
    if callable(k):
        return k(v)
    else:
        return v[k]


def smart_sort(xs, getters_and_orders):
    orders = []
    getters = []
    for x in getters_and_orders:
        if isinstance(x, tuple):
            g, o = x
        else:
            g = x
            o = "desc"
        getters.append(g)
        orders.append(o)

    @total_ordering
    class _Sorter:
        def __init__(self, v):
            self._values = [_apply(v, g) for g in getters]

        def __eq__(self, other):
            return self._values == other._values

        def __lt__(self, other):
            for (a, b, o) in zip(self._values, other._values, orders):
                if a != b:
                    return a > b if o == "desc" else a < b
            return False

    return sorted(xs, key=_Sorter)


xs = [("a", "b", "e"), ("b", "a", "c"), ("b", "a", "d")]
ret = smart_sort(xs, [(1, "asc"), (2, "desc")])
print(ret)

>> [('b', 'a', 'd'), ('b', 'a', 'c'), ('a', 'b', 'e')]
Du Shiqiao
  • 377
  • 1
  • 9
1

You can use two sorts (they're stable) in order of decreasing priority:

>>> from operator import itemgetter
>>> a = [("a", "b", "e"), ("b", "a", "c"), ("b", "a", "d")]

>>> a.sort(key=itemgetter(2), reverse=True)
>>> a.sort(key=itemgetter(1))

>>> a
[('b', 'a', 'd'), ('b', 'a', 'c'), ('a', 'b', 'e')]

Last time I checked, this was btw faster than a single sort with a more complicated key function. And it takes less memory than a key function creating new tuples.

See Python's Sorting HOW TO for more examples of this.

Kelly Bundy
  • 23,480
  • 7
  • 29
  • 65