Home > Mobile >  How to sort list of objects by multiple fields that are strings asc/desc in Python?
How to sort list of objects by multiple fields that are strings asc/desc in Python?

Time:10-13

I want to sort list of objects based on user input where I allow to sort some columns in ASC order and others in DESC order (that fields are strings/numbers - but with numbers there is no problem).

Example class and list of objects looks like this:

from pydantic import BaseModel

class Person(BaseModel):
  firstname: str
  lastname: str
  weight: float

people = [
  Person(firstname="John", lastname="Snow", weight=100.1), 
  Person(firstname="Matthew", lastname="Smith", weight=98.7), 
  # And more...
]

So the problem is that I want to sort objects by first name ASC, last name DESC (and other fields also DESC or ASC).

I tried to do it like people.sort(lambda person: (-person.firstname, person.lastname))

But I got TypeError: bad operand type for unary -: 'str'.

CodePudding user response:

To sort with any arbitrary ordering of the attributes, it's best to define the magic methods __lt__, __le__, __gt__, __ge__, and __eq__g. With funtools.total_ordering decorator, you just need to define any of the 1st four and __eq__

Since __eq__ is already defined in BaseModel, you just need one of __lt__, __le__, __gt__, __ge__.

For example with __lt__

from pydantic import BaseModel
from functools import total_ordering

@total_ordering
class Person(BaseModel):
    firstname: str
    lastname: str
    weight: float

    def __lt__(self, other):
        # first sort firstname in ascending order
        less_than = (self.firstname < other.firstname)
        # if firstname is equal, sort by lastname in descending order
        less_than |= ((self.firstname == other.firstname) & (self.lastname > other.lastname))
        # if both firstname and last are equal, sort by weight in ascending order
        less_than |= ((self.firstname == other.firstname) & (self.lastname == other.lastname) & (self.weight < other.weight))
        return less_than

Example running the sort:

>>> people = [
  Person(firstname="John", lastname="Snow", weight=100.1), 
  Person(firstname="Matthew", lastname="Smith", weight=98.7), 
  Person(firstname="Matthew", lastname="Adam", weight=98.7), 
  Person(firstname="Matthew", lastname="Smith", weight=100), 
]
>>> people.sort()
>>> people
[Person(firstname='John', lastname='Snow', weight=100.1), 
Person(firstname='Matthew', lastname='Smith', weight=98.7),
Person(firstname='Matthew', lastname='Smith', weight=100.0), 
Person(firstname='Matthew', lastname='Adam', weight=98.7)]

CodePudding user response:

from pydantic import BaseModel


class NegatedableStr(str):
    def __neg__(self):
        return list(map(lambda c: -ord(c), self))


class Person(BaseModel):
    firstname: str
    lastname: str
    weight: float

    def __init__(self, **kwargs):
        for key, val in kwargs.items():
            if isinstance(val, str):
                kwargs[key] = NegatedableStr(val)
        super().__init__(**kwargs)


people = [
    Person(firstname="John", lastname="Snow", weight=100.1), 
    Person(firstname="Matthew", lastname="Smith", weight=98.7),
]

people.sort(key=lambda person: (
    -person.firstname,
    person.lastname,
    person.weight,
))
print(*people, sep='\n')

Output:

firstname='Matthew' lastname='Smith' weight=98.7
firstname='John' lastname='Snow' weight=100.1
  • Related