124

I was refactoring some old code of mine and came across of this:

alist.sort(cmp_items)

def cmp_items(a, b):
    if a.foo > b.foo:
        return 1
    elif a.foo == b.foo:
        return 0
    else:
        return -1

The code works (and I wrote it some 3 years ago!) but I cannot find this thing documented anywhere in the Python docs and everybody uses sorted() to implement custom sorting. Can someone explain why this works?

Marcellinov
  • 311
  • 7
  • 18
Lorenzo
  • 4,558
  • 11
  • 44
  • 54
  • `sorted()` and `sort()` offer custom sorting in much the same way, modulo the difference in calling convention. – Russell Borogove Aug 07 '12 at 17:44
  • 2
    Indeed, what happens is that using a `key` parameter is preferred over passing a `cmp` function. (The later is not even implemented in Python 3) – jsbueno Aug 08 '12 at 02:50
  • It's kind of ambiguous, depends on what the items in the list were; your code requires that they have an attribute `foo`, otherwise it blows up. Better to define a custom `__lt__()` method for your class, then `sorted()` and `list.sort()` will work out-of-the-box. (Btw, objects no longer need to define `__cmp__()`, just `__lt__()` . [See this](https://stackoverflow.com/questions/1061283/lt-instead-of-cmp()) – smci Sep 10 '18 at 03:00

6 Answers6

142

As a side note, here is a better alternative to implement the same sorting:

alist.sort(key=lambda x: x.foo)

Or alternatively:

import operator
alist.sort(key=operator.attrgetter('foo'))

Check out the Sorting How To, it is very useful.

Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
70

It's documented here.

The sort() method takes optional arguments for controlling the comparisons.

cmp specifies a custom comparison function of two arguments (list items) which should return a negative, zero or positive number depending on whether the first argument is considered smaller than, equal to, or larger than the second argument: cmp=lambda x,y: cmp(x.lower(), y.lower()). The default value is None.

Charlie
  • 8,530
  • 2
  • 55
  • 53
miles82
  • 6,584
  • 38
  • 28
  • Thanks miles82 I was checking here and couldn't see it in the method signature http://docs.python.org/tutorial/datastructures.html – Lorenzo Aug 07 '12 at 16:48
  • I don't see the same text on the page you linked to. Did the documentation change. Besides, when I try to use `cmp`, I get `TypeError: 'cmp' is an invalid keyword argument for this function`. What is going on here? – HelloGoodbye Aug 16 '19 at 10:17
  • 4
    @HelloGoodbye sort() doesn't have a cmp argument in Python 3. This is an old answer when the docs link was for Python 2. You can find the old docs [here](https://docs.python.org/2/library/stdtypes.html#mutable-sequence-types) or read more about it [here](https://docs.python.org/3/howto/sorting.html#the-old-way-using-the-cmp-parameter). If you're using Python 3, use the [key argument](https://docs.python.org/3/library/stdtypes.html#list.sort) instead. – miles82 Aug 17 '19 at 22:45
  • And what if you actually want to provide a comparison function? I want to treat numbers in a string (of any length, picked out greedily) as symbols, equivalently to how individual characters are otherwise treated. I know how to achieve that trivially if I may provide a comparison function, but not if I must provide a key function. Why was this changed? – HelloGoodbye Aug 19 '19 at 00:48
  • I guess it can still be achieved if each number contained in the string is encoded using an encoding that orders the numbers lexicographically, such as [Levenshtein coding](https://en.m.wikipedia.org/wiki/Levenshtein_coding). But I consider this more as a workaround to the fact that `sort` doesn’t take a comparison function as argument in Python 3, and not as something that I *actually* would like to do. – HelloGoodbye Aug 19 '19 at 01:18
  • @HelloGoodbye you can use [functools.cmp_to_key](https://docs.python.org/3/library/functools.html#functools.cmp_to_key) to transform an old-style comparison function to a key function. – miles82 Aug 19 '19 at 11:09
  • @miles82 Thanks! – HelloGoodbye Aug 19 '19 at 16:58
  • Need sample code to be accepted in my view. – Nam G VU Mar 10 '21 at 00:31
33

Just like this example. You want sort this list.

[('c', 2), ('b', 2), ('a', 3)]

output:

[('a', 3), ('b', 2), ('c', 2)]

you should sort the tuples by the second item, then the first:

def letter_cmp(a, b):
    if a[1] > b[1]:
        return -1
    elif a[1] == b[1]:
        if a[0] > b[0]:
            return 1
        else:
            return -1
    else:
        return 1

Then convert it to a key function:

from functools import cmp_to_key
letter_cmp_key = cmp_to_key(letter_cmp))

Now you can use your custom sort order:

[('c', 2), ('b', 2), ('a', 3)].sort(key=letter_cmp_key)
mit
  • 11,083
  • 11
  • 50
  • 74
RryLee
  • 535
  • 4
  • 6
21

This does not work in Python 3.

You can use functools cmp_to_key to have old-style comparison functions work though.

from functools import cmp_to_key

def cmp_items(a, b):
    if a.foo > b.foo:
        return 1
    elif a.foo == b.foo:
        return 0
    else:
        return -1

cmp_items_py3 = cmp_to_key(cmp_items)

alist.sort(key = cmp_items_py3)
Neel Sandell
  • 429
  • 5
  • 12
The Unfun Cat
  • 29,987
  • 31
  • 114
  • 156
  • 1
    `cmp_to_key` also works when used as a no-argument decorator (put `@cmp_to_key` on the line before your `def` for the comparing function) so you don't need to call `cmp_to_key` and assign the result yourself – Samathingamajig May 22 '21 at 02:54
13

I know many have already posted some good answers. However I want to suggest one nice and easy method without importing any library.

l = [(2, 3), (3, 4), (2, 4)]
l.sort(key = lambda x: (-x[0], -x[1]) )
print(l)
l.sort(key = lambda x: (x[0], -x[1]) )
print(l)

Output will be

[(3, 4), (2, 4), (2, 3)]
[(2, 4), (2, 3), (3, 4)]

The output will be sorted based on the order of the parameters we provided in the tuple format

  • 2
    For sorting, first, it will check the first item in the tuple and it will try to sort according to the sign you have given ('-' indicates reverse sorting). If it fails to sort using the first item, then it considers the second item in the tuple..and so on... – Ashrith Kumar Peddi Jan 20 '21 at 10:57
6

Even better:

student_tuples = [
    ('john', 'A', 15),
    ('jane', 'B', 12),
    ('dave', 'B', 10),
]

sorted(student_tuples, key=lambda student: student[2])   # sort by age
[('dave', 'B', 10), ('jane', 'B', 12), ('john', 'A', 15)]

Taken from: https://docs.python.org/3/howto/sorting.html

Steven
  • 4,826
  • 3
  • 35
  • 44