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 isNone
(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 they
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 onz
.
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
Installing Numpy with Pip on Windows 10 for Python 3.7
Python Datetime Formatting Without Zero-Padding
How to Perform Two-Dimensional Interpolation Using Scipy
Installing Multiple Versions of a Package with Pip
How to Do Virtual File Processing
How to Change Effective Process Name in Python
How to Read Two Lines from a File at a Time Using Python
Validating Detailed Types in Python Dataclasses
How to Configure Atom to Run Python3 Scripts
Constructing a Co-Occurrence Matrix in Python Pandas
Python Equivalent of Filter() Getting Two Output Lists (I.E. Partition of a List)
Python Urllib2 with Keep Alive
How to Remove the Space Between Subplots in Matplotlib.Pyplot
Smtpauthenticationerror When Sending Mail Using Gmail and Python