-1

In Python 2.7 there was an ability to use the sorted function with a list that contains different types, e.g. string, tuple, integer, like this:

some_list = ['a', 'b', 1, ('c', 'd'), [2, 3, 4]]

>>> sorted(some_list)

[1, [2, 3, 4], 'a', 'b', ('c', 'd')]

However, if we try to do something like this in Python 3, we will get an exception:

some_list = ['a', 'b', 1, ('c', 'd'), [2, 3, 4]]

>>> sorted(some_list)

TypeError: '<' not supported between instances of 'int' and 'str'

What's the easiest way to mimic the behavior of the sorted function from python2.7? I understand that I can just write my own method for this purposes, but maybe a similar functionality was implemented in some library or moved to another specific built-in function?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Helen
  • 463
  • 2
  • 9
  • 23
  • Have you looked at the 3.0 release notes to figure out what changed for `sorted()`? – Ulrich Eckhardt Mar 11 '22 at 16:23
  • Yes, it says that the ordering comparison operators (<, <=, >=, >) raise a TypeError exception when the operands don’t have a meaningful natural ordering. Thus, expressions like `1 < ''`, `0 > None` or `len <= len` are no longer valid, and e.g. `None < None` raises TypeError instead of returning False. A corollary is that sorting a heterogeneous list no longer makes sense – all the elements must be comparable to each other. Note that this does not apply to the == and != operators: objects of different incomparable types always compare unequal to each other. – Helen Mar 11 '22 at 16:35

2 Answers2

1

You need to supply an explicit key to sort or sorted, and the key should explain how to compare elements of different types.

A quick solution is to replace element x with a tuple (type(x).__name__, x) so that elements are grouped by type first, and only compared if they have the same type:

some_list = ['a', [4, 5, 6], 3, 2.0, 'b', 1, 1.5, ('c', 'd'), [2, 3, 4]]

some_list.sort(key=lambda x: (type(x).__name__, x))

print(some_list)
# [1.5, 2.0, 1, 3, [2, 3, 4], [4, 5, 6], 'a', 'b', ('c', 'd')]

Note how the integers were separated from the floats. If you don't want that, you need a more complex key. I give an example at the bottom of this post.

You could try other keys, which will result in different orders:

  • key=str or key=repr: Brutally convert everything to string before comparison. This has the downside that numbers are compared lexicographically, so that 1 < 10 < 2.
  • key=lambda x: tuple(more_itertools.collapse(x)) this flattens everything into tuples, so that a single number is equivalent to a list of one number, and a list of lists of lists is equivalent to a simple list. But it will still crash if trying to compare numbers with strings.
  • Some hybrid method of the two solutions above. You can declare a complex key like any function, using the def keyword.
  • Try comparing the elements with their usual comparison, then if it raises an exception, convert the elements into strings and compare again:
from functools import cmp_to_key

def cmp(a,b):
    try:
        return (a > b) - (a < b)
    except TypeError:
        a, b = str(a), str(b)
        return (a > b) - (a < b)

k = cmp_to_key(cmp)

some_list = ['a', [4, 5, 6], 3, 2.0, 'b', 1, 1.5, ('c', 'd'), [2, 3, 4]]

some_list.sort(key=k)

print(some_list)
# [('c', 'd'), 1, 1.5, 2.0, 3, [2, 3, 4], [4, 5, 6], 'a', 'b']
Stef
  • 13,242
  • 2
  • 17
  • 28
0

You ought to be able to get what you're after with an appropriate key function. E.g.:

>>> some_list = ['a', 'b', 1, ('c', 'd'), [2, 3, 4]]
>>> sorted(some_list, key=lambda i: str(i[0] if hasattr(i, "__getitem__") else i))
[1, [2, 3, 4], 'a', 'b', ('c', 'd')]

The details of the key will depend on exactly how you want to handle comparisons between items of differing types -- the example above assumes you want to use string-based comparison, but there might be other methods.

Samwise
  • 68,105
  • 3
  • 30
  • 44
  • It is worth stating that this loses the natural order for numbers; for instance `some_list = [1, 12, 2]` will not be sorted into [1, 2, 12]. – Stef Mar 11 '22 at 16:41