Reputation: 1725
For example, if I have a Person
class
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
def __repr__(self) -> str:
return f"({self.name}, {self.age})"
and a List of Person
persons = [
Person("Bob", 25),
Person("Alice", 25),
Person("Charlie", 23),
Person("Dave", 25),
]
I can sort the list by age
in ascending order, and in case of a tie, sort by name
in ascending order using the following method:
sorted_persons = sorted(persons, key=lambda p: (p.age, p.name))
Question:
However, I'm looking for a way to sort the list by age
in ascending order and, in the event of a tie in age, sort by name
in descending order. How could I achieve this in Python?
I've come up with one solution, as shown below, but it seems a bit inelegant. Is there a more succinct way to write a string comparison method that can handle all three cases (i.e., less than, equal to, and greater than)? For instance, Java has a s1.compareTo(s2)
method that makes such comparisons straightforward.
Here's the solution I'm currently working with:
from functools import cmp_to_key
def compare(p1, p2):
cmp = p1.age - p2.age
if cmp != 0:
return cmp
if p1.name < p2.name:
return 1
elif p1.name > p2.name:
return -1
return 0
sorted_persons = sorted(persons, key=cmp_to_key(compare))
This code correctly sorts the persons
list first by age
in ascending order, and then by name
in descending order when the ages are equal. However, I feel there should be a cleaner, more Pythonic way to handle this. Any suggestions?
Upvotes: 2
Views: 1255
Reputation: 191
The generic solution of creating the descending class from Alain T. is definitely a better way to solve this generically. One little improvement - __eq__
must also be implemented. Otherwise, the sort attribute following it will not be considered as equal and thus retaining the original list order.
In the example below, without the __eq__
, for age 25 and salary 60000, it will be ordered as Charlie->Ally->Billy.
class descending():
def __init__(self,value): self.value = value
def __lt__(self, other): return self.value > other.value
def __eq__(self, other): return self.value == other.value
data = [
{"name": "Alice", "age": 30, "salary": 50000},
{"name": "Bob", "age": 25, "salary": 70000},
{"name": "Charlie", "age": 25, "salary": 60000},
{"name": "Ally", "age": 25, "salary": 60000},
{"name": "Billy", "age": 25, "salary": 60000},
]
data.sort(key=lambda x: (x["age"], descending(x["salary"]), x["name"]))
Upvotes: 0
Reputation: 42133
Python's sort is stable. This means that the order of items that have the same sort key is preserved in the resulting list.
You can leverage this by sorting in steps, starting with the least significant order:
sorted_persons = sorted(persons, key=lambda p: p.name, reverse=True)
sorted_persons.sort(key=lambda p: p.age)
You could also streamline the cmp_to_key to a lambda (but that's not a different solution):
sorted_persons = sorted(persons,key=cmp_to_key(
lambda a,b: a.age-b.age or (a.name<b.name)-(b.name<a.name)))
For a more general solution, you could create a class that will handle all descending key ordering expressed directly in the key=lambda's return tuples:
class descending():
def __init__(self,value): self.value = value
def __lt__(self,other): return self.value > other.value
usage:
sorted_persons = sorted( persons, key=lambda p:(p.age,descending(p.name)) )
Upvotes: 2
Reputation: 195573
Another solution:
For the purpose of sorting you can define custom str
class where you override the __lt__
magic method (less than):
class reverse_cmp_string(str):
def __lt__(self, other):
return not str.__lt__(self, other)
sorted_persons = sorted(persons, key=lambda p: (p.age, reverse_cmp_string(p.name)))
print(sorted_persons)
Prints:
[(Charlie, 23), (Dave, 25), (Bob, 25), (Alice, 25)]
Upvotes: 4
Reputation: 134038
Python used to have the cmp
built-in function which did what you want.
There was also a keyword argument callled cmp
for sort
and sorted
, which is what you could have used here, but it was removed in Python 3.0+ because the key
was often more performant than cmp
. Unfortunately you found the one where it quite isn't.
However there is a trick you could do here: you can use tuple comparison and you can swap the names, therefore:
def compare(p1, p2):
a = p1.age, p2.name
b = p2.age, p1.name
then you can just compare these two. There is also a trick to nicely produce -1, 0, 1 by using the fact that True and False are integers with values of 1 and 0 respectively:
return (a > b) - (a < b)
if >
and <
are normally defined then maximum one side will result in True, with the other being False; True - False == 1
and False - True == -1
, and False - False == 0
.
Upvotes: 2
Reputation: 47
This snippet of code should do it. I ran it and the output looks right
sorted_persons = sorted(persons, key=lambda p: (-p.age, p.name), reverse=True)
Upvotes: 2