What is a pythonic way of using multiple fields for key evaluation during sorting?

Viewed 69

Python3 removed the cmp parameter which was used for comparison during sorting and replaced it with key.

The cmp_to_key function was introduced to be "primarily used as a transition tool for programs being converted from Python 2".

It is unclear to me how to recreate cmp behavior natively in python3 with key. For example, if I want to sort strings by length, with a tie-breaker by lexicographic order, I would write something like this:

def compare(word1, word2):
    if len(word1) > len(word2):
        return 1
    if len(word2) > len(word1):
        return -1
    if word1 < word2:
        return 1
    if word2 < word1:
        return -1
    return 0

myStrings.sort(key=functools.cmp_to_key(compare), reverse=True)

How can a comparison across objects be done in a python3-native way, meaning without cmp_to_key?

One thing I can think of is returning a more complex key, for example:

# This would return something like 5.12 for "hello" and 5.20 for "world"
def myKey(word):
    return len(word) + myLexicographicalValueFunction(word)/100

myStrings.sort(key=myKey, reverse=True)

But this doesn't seem to scale well as you add more variables to compare. What is a pythonic way of using multiple fields for key evaluation during sorting in python3?

2 Answers

For that specific comparison, you could use a tuple as the key:

myStings.sort(key=lambda s:(-len(s),s),reverse=True)

For object classes, you would probably need to implement a comparison operator (e.g. def __lt__(self,other): method) to let the sort know how to compare them natively.

If you can't modify the object class or if your comparison is very complex, you could create a wrapper object which implements the __lt__() method using a lambda that you povide:

class ObjectComp:
    def __init__(self,instance,cmp):
        self.instance = instance
        self.cmp      = cmp
        
    def __lt__(self,other):
        return self.cmp(self.instance,other.instance)
        
def cmpsort(objects,cmp):
    return [obj.instance for obj in sorted(ObjectComp(o,cmp) for o in objects)]


s = cmpsort([1,11,21,12,13],cmp=lambda a,b:a%10>b%10)
print(s)
[13, 12, 1, 11, 21]

If you have a complicated comparison, you are applying a distinctive meaning to the string that in pythonic terms probably means it would benefit from its own type. Python is happy to allow you to subclass generic types so in your case you probably want something like this:

class InterestingWord(str):

    def __lt__(self, x: str) -> bool:
        if len(self) < len(x):
            return True
        return super().__lt__(x)

    def __gt__(self, x: str) -> bool:
        if len(self) > len(x):
            return True
        return super().__gt__(x)

Thus:

In [2]: InterestingWord("00") > InterestingWord("1")
Out[2]: True

In [3]: "00" > "1"
Out[3]: False
Related