code kid
code kid

Reputation: 414

reverse second sort characteristic in python sorted()

I'm trying to sort a list and I can't figure out to reverse the order of a second sorting characteristic.

import math
ps = {1:(1,1),2:(3,2),3:(3,-3),4:(-3,4),5:(-2,-2),6:(3,3),7:(1,-1)} 
l = []
for x in range(1,8):

    l.append((math.atan2(ps[x][1],ps[x][0]),ps[x]))

for c in sorted(l, key = lambda t:(t[0], math.sqrt((0-t[1][0])**2 + (0-t[1][1])**2)),reverse = True):
    print(c)

The first sort characteristic is sorting by angle, the second sorts by distance from the origin if the angle is equal. Does anyone know how to do this. Thanks in advance for the help.

Upvotes: 3

Views: 1500

Answers (4)

MSeifert
MSeifert

Reputation: 152765

If your key-function get's longish and/or complicated you can also use a custom class that compares like you want. It's a bit slower but it can be more readable, especially because you can comment the code:

import math

class Angle_Distance(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.angle = math.atan2(self.y, self.x)
        self.distance = math.hypot(self.x, self.y)

    def __repr__(self):
        return '{self.__class__.__name__}(x={self.x}, y={self.y})'.format(self=self)

    def __eq__(self, other):
        # this method is not strictly necessary, but makes the class more generally useful.
        return self.x == other.x and self.y == other.y

    def __lt__(self, other):
        if self.angle < other.angle:
            return True
        elif self.angle == other.angle:  # break ties
            return self.distance > other.distance
        else:
            return False

And this can be applied on the list:

>>> ps = {1:(1,1),2:(3,2),3:(3,-3),4:(-3,4),5:(-2,-2),6:(3,3),7:(1,-1)} 
>>> l = [Angle_Distance(i, j) for i, j in ps.values()]
>>> sorted(l, reverse=True)
[Angle_Distance(x=-3, y=4),
 Angle_Distance(x=1, y=1),
 Angle_Distance(x=3, y=3),
 Angle_Distance(x=3, y=2),
 Angle_Distance(x=1, y=-1),
 Angle_Distance(x=3, y=-3),
 Angle_Distance(x=-2, y=-2)]

But you could also use it as key-function:

>>> ps = {1:(1,1),2:(3,2),3:(3,-3),4:(-3,4),5:(-2,-2),6:(3,3),7:(1,-1)}
>>> sorted(ps.values(), key=lambda x: Angle_Distance(x[0], x[1]), reverse=True)
[(-3, 4), (1, 1), (3, 3), (3, 2), (1, -1), (3, -3), (-2, -2)]

Upvotes: 1

PM 2Ring
PM 2Ring

Reputation: 55499

You already have several good answers, but I thought you might appreciate another one. ;)

The answer by Paul Cornelius shows the simple way to do this: just negate one of the numbers produced by your key function. However, as Mark Ransom mentioned, you can only do that with numeric values, but fortunately Python's TimSort is stable. So you can sort a list several times on multiple criteria and each subsequent sort won't disturb the items that are equal according to the current sort key function. It's a little less efficient to sort in multiple passes, so it's better to use Paul's technique when you can. OTOH, TimSort is very efficient at processing partially sorted lists, so when doing multi-pass sorts the additional passes will usually run fairly quickly.

You can create your l list a little more efficiently by using a list comprehension. But even if you don't want to use a list comp it would be better to loop directly over the values of ps rather than using a range - it's more efficient and also more general, since it works if the keys aren't a contiguous range. The values may not be in the numeric order of the keys (although they will be in Python 3.6+), but that doesn't matter here since we're going to sort l anyway.

Here's a demo:

import math

ps = {
    1: (1, 1), 2: (3, 2), 3: (3, -3), 4: (-3, 4), 
    5: (-2, -2), 6: (3, 3), 7: (1, -1),
}

l = []
for t in ps.values():
    l.append((math.atan2(t[1], t[0]), t))

for t in l:
    print(t)

output

(0.7853981633974483, (1, 1))
(0.5880026035475675, (3, 2))
(-0.7853981633974483, (3, -3))
(2.214297435588181, (-3, 4))
(-2.356194490192345, (-2, -2))
(0.7853981633974483, (3, 3))
(-0.7853981633974483, (1, -1))

Using a list comp we can build l in one line instead of three lines:

l = [(math.atan2(t[1], t[0]), t) for t in ps.values()]

We can shorten that slightly by using extended slicing to reverse t and the * "splat" operator to unpack the reversed t:

l = [(math.atan2(*t[::-1]), t) for t in ps.values()]

Your expression

math.sqrt((0-t[1][0])**2 + (0-t[1][1])**2)

is overly verbose. (0-x)**2 equals x**2, so we can rewrite that expression as

math.sqrt(t[1][0]**2 + t[1][1]**2)

However, there's an even better way. The math module has a hypot function, which MSeifert has used. hypot(x, y) calculates the length of the hypotenuse of a right triangle with sides x and y, so your expression could be written as

math.hypot(t[1][0], t[1][1])

or using tuple unpacking,

math.hypot(*t[1])

Putting it all together:

import math

ps = {
    1: (1, 1), 2: (3, 2), 3: (3, -3), 4: (-3, 4), 
    5: (-2, -2), 6: (3, 3), 7: (1, -1),
}

l = [(math.atan2(*t[::-1]), t) for t in ps.values()]
l.sort(key=lambda t: (-t[0], math.hypot(*t[1])))
for t in l:
    print(t)

output

(2.214297435588181, (-3, 4))
(0.7853981633974483, (1, 1))
(0.7853981633974483, (3, 3))
(0.5880026035475675, (3, 2))
(-0.7853981633974483, (1, -1))
(-0.7853981633974483, (3, -3))
(-2.356194490192345, (-2, -2))

If you only want to sort the tuples and don't need to keep the angles, you can do it like this:

l = sorted(ps.values(), key=lambda t: (-math.atan2(*t[::-1]), math.hypot(*t)))
print(l)

output

[(-3, 4), (1, 1), (3, 3), (3, 2), (1, -1), (3, -3), (-2, -2)]

Upvotes: 1

Mark Ransom
Mark Ransom

Reputation: 308452

You can do two sorts, least significant first. Python sorting is stable, so the order determined by the first sort will hold when you do the second.

for c in sorted(sorted(l, key = lambda t:math.sqrt((0-t[1][0])**2 + (0-t[1][1])**2)), key = lambda t:t[0], reverse=True):

This method works even when the keys aren't numeric.

Upvotes: 3

Paul Cornelius
Paul Cornelius

Reputation: 10946

Put a minus sign in front of the second sort characteristic:

for c in sorted(l, key = lambda t:(t[0], -math.sqrt((0-t[1][0])**2 + (0-t[1][1])**2)),reverse = True):
    print(c)

Upvotes: 3

Related Questions