How to Use a Custom Comparison Function in Python 3

How to use a custom comparison function in Python 3?

Use the key argument (and follow the recipe on how to convert your old cmp function to a key function).

functools has a function cmp_to_key mentioned at docs.python.org/3.6/library/functools.html#functools.cmp_to_key

Custom compare function in python3

1) Your my_cmp() function is supposed to return one of three values (+, -, or 0 depending upon the compare), but you only return two (True and False).

2) You ingnore the return value from sorted(). sorted() doesn't modify its argument, it returns a sorted copy of it.

3) Don't use cmp functions. They are hard to describe and hard to implement. Instead use key functions.

How about:

def my_key(point1):
return point1[0]*point1[0] + point1[1]*point1[1]

points = [ [3.1, 4.1], [0.9, 0.8], [1.0, 1.0] ]
points = sorted(points, key=my_key)
print(points)

assert points == [ [0.9, 0.8], [1.0, 1.0], [3.1, 4.1] ]

Sort a list of lists with a custom compare function

>>> l = [list(range(i, i+4)) for i in range(10,1,-1)]
>>> l
[[10, 11, 12, 13], [9, 10, 11, 12], [8, 9, 10, 11], [7, 8, 9, 10], [6, 7, 8, 9], [5, 6, 7, 8], [4, 5, 6, 7], [3, 4, 5, 6], [2, 3, 4, 5]]
>>> sorted(l, key=sum)
[[2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8], [6, 7, 8, 9], [7, 8, 9, 10], [8, 9, 10, 11], [9, 10, 11, 12], [10, 11, 12, 13]]

The above works. Are you doing something different?

Notice that your key function is just sum; there's no need to write it explicitly.

Sorting key function that uses custom comparison

Use the functools.cmp_to_key helper.

>>> import functools
>>> digits = ['3', '30', '34', '32', '9', '5']
>>> sorted(
... digits,
... key=functools.cmp_to_key(lambda a, b: cmp(a+b, b+a)),
... reverse=True)
['9', '5', '34', '3', '32', '30']

The Python 3 sorting functions take a ‘key’ parameter:

key specifies a function of one argument that is used to extract a
comparison key from each list element: key=str.lower. The default
value is None (compare the elements directly).

The functools.cmp_to_key helper is designed to help you transition to
that style:

functools.cmp_to_key(func)

Transform an old-style comparison function to a key function. […]
This function is primarily used as a transition tool for programs
being converted from Python 2 which supported the use of comparison
functions.

This works in the latest Python 2 and Python 3.

The trick is done by creating a key function that takes an item for
comparison, and returns a custom object, which knows how to compare
itself as specified by your comparison function.

>>> key_func = functools.cmp_to_key(lambda a, b: cmp(a+b, b+a))
>>> key_func("32")
<functools.K object at 0x7f6781ce0980>
>>> key_func("32") < key_func("5")
True

See the Sorting HOWTO for this and other tricks.

Custom comparison functions for built-in types in Python

Let's say you have this class:

class Thingy(object):
def __init__(self, key, notkey):
self.key, self.notkey = key, notkey
def __eq__(self, other):
return self.key == other.key
def __hash__(self):
return hash(self.key)

Now, you want to put these in a set, but keyed by notkey instead of key. You can't do that as-is, because a set expects its elements to have a consistent meaning for equality—and also a consistent meaning for hash such that a == b always implies hash(a) == hash(b). So, create a wrapper:

class WrappedThingy(object):
def __init__(self, thingy):
self.thingy = thingy
def __eq__(self, other):
return self.thingy.notkey == other.thingy.notkey
def __hash__(self):
return hash(self.thingy.notkey)

And you can put those in a set:

wts = set(WrappedThingy(thingy) for thingy in thingies)

For example, let's say you want to uniquify your thingies, keeping exactly one thingy (arbitrarily) for each notkey value. Just wrap them, stick the the wrappers in a set, then unwrap them and stick the unwrappees in a list:

wts = set(WrappedThingy(thingy) for thingy in thingies)
thingies = [wt.thingy for wt in wts]

This is part of a more general Python pattern called "DSU". This stands for "decorate-sort-undecorate", which is wildly inaccurate nowadays, since you almost never need it for sorting-related tasks in modern Python… but historically it made sense. Feel free to call it "decorate-process-undecorate" in hopes that it will catch on, but don't hope too hard.

The reason you don't need DSU for sorting nowadays is that most sorting functions all take key functions as arguments. In fact, even for uniquifying, the unique_everseen function in the itertools recipes takes a key.

But if you look at what it does under the covers, it's basically DSU:

for element in iterable:
k = key(element)
if k not in seen:
seen.add(k)
yield element

(The fact that it's a generator rather than a list-building function means it can "undecorate on the fly", which makes things a bit simpler. But otherwise, same idea.)

How to convert a Python 2 comparison function to a Python 3 key function?

Consider how tuples normally compare: element by element, going to the next element when the current values are equal (sometimes called lexicographic order).

Our required comparison algorithm, rewritten in steps that match that general approach, is:

  • First, we want to compare the x values, putting them in ascending order.
  • Then we want to compare the z values; we want tuples with an S to come first. This is the opposite of what normally happens, and we can't easily specify reverse order for only part of the key, and we can't negate a string value. However, since only S and E are possible, we can simply map S to 0 and E to 1. Or more simply, S can map to False and E to True, since those are numerically equal to 0 and 1 respectively.
  • Finally, if the z values were equal, we want to compare the y values - in normal order if we have a E, and in reverse order (so, negate the numeric values) if we have a S.

So, we create a key that corresponds to that rule:

  • The first element is x.
  • The second element is our translation of the original z value.
  • The third element is the y value, possibly negated depending on z.

In code:

def fancy_key(xyz: tuple[int, int, str]) -> tuple[int, bool, int]:
x, y, z = xyz
is_e = z == 'E'
return (x, is_e, (y if is_e else -y))

Using a comparator function to sort

You're passing the comparator as the key function. You should be passing it as the cmp, wrapped in some kind of function that turns it into a proper comparator.

def make_comparator(less_than):
def compare(x, y):
if less_than(x, y):
return -1
elif less_than(y, x):
return 1
else:
return 0
return compare

sortedDict = sorted(subjects, cmp=make_comparator(cmpValue), reverse=True)

(Although actually, you should be using key functions:

sorted(subjects, operator.itemgetter(0), reverse=True)

Also note that sortedDict will not actually be a dict, so the name is rather confusing.)



Related Topics



Leave a reply



Submit