2
votes

In Python you can for example sort tuples sorted([(2,2),(1,2),(2,1),(1,1)]) and get [(1, 1), (1, 2), (2, 1), (2, 2)].

You can also use custom key functions sorted([x, y, z], key=custom_key) to implement special sorting criteria.

For example:

medals_map = {'Gold': 1, 'Silver': 2, 'Bronze': 3 }
def custom_key(item):
    if isinstance(item, basestring):
       try:
           return medals_map[item]
       except KeyError:
           pass
    return item

Could sort any list of strings normally, and also gold/silver/bronze according to my custom ordering.

But it does not "correctly" handle tuples anymore: sorted([('Gold', 2), ('Bronze', 1), ('Gold', 1)], key=custom_key) results in [('Bronze', 1), ('Gold', 1), ('Gold', 2)] as item is not a basestring.

But I would like to get [('Gold', 1), ('Gold', 2), ('Bronze', 1)] by somehow applying the custom logic to each item in the tuple.

What's the best way to "map" the custom key logic over any tuples (that may or may not appear) in the list like the default key function does?


Edit: More examples:

  • sorted(['Silver', 'Bronze', 'Gold'], key=custom_key)
    • ['Gold', 'Silver', 'Bronze']
  • sorted([['Silver', 2], ['Silver', 1], ['Gold', 1]], key=custom_key)
    • [['Gold', 1], ['Silver', 1], ['Silver', 2]]
  • sorted([(2, 'Gold'), (2, 'Bronze'), (1, 'Gold')], key=custom_key)
    • [(1, 'Gold'), (2, 'Gold'), (2, 'Bronze')]
  • sorted([('Silver', 'Bronze'), ('Gold', 'Bronze'), ('Silver', 'Gold')], key=custom_key)
    • [('Gold', 'Bronze'), ('Silver', 'Gold'), ('Silver', 'Bronze')]
4
So the custom_key needs to handle item being either a tuple or string? Why not add another isinstance, with a recursive call? Note that return medals_map.get(item, item) is neater than faffing with try and except. - jonrsharpe
@jonrsharpe: Right, but item might also be a list, or maybe some other iterable? Do I just have to add more isinstance checks for all kinds of things? I hoped since the default key function already does all that I can somehow avoid doing that. - Peter
@Peter, add an example of a less structured input list and what you expect as output - Padraic Cunningham
@PadraicCunningham: I now added more examples at the end of the question - Peter
@Peter if you don't even know what you're going to be trying to sort, maybe solve that problem? - jonrsharpe

4 Answers

2
votes
from collections import Iterable
def custom_key(item):
    if isinstance(item, Iterable) and not isinstance(item, basestring):
        return [medals_map.get(ele, ele) for ele in item]
    return medals_map.get(item, item)

Output:

In [2]: assert sorted(['Silver', 'Bronze', 'Gold'], key=custom_key) == ['Gold', 'Silver', 'Bronze']

In [3]: assert sorted([['Silver', 2], ['Silver', 1], ['Gold', 1]], key=custom_key) == [['Gold', 1], ['Silver', 1], ['Silver', 2]]

In [4]: assert sorted([(2, 'Gold'), (2, 'Bronze'), (1, 'Gold')], key=custom_key) == [(1, 'Gold'), (2, 'Gold'), (2, 'Bronze')]

In [5]: assert sorted([('Silver', 'Bronze'), ('Gold', 'Bronze'), ('Silver', 'Gold')], key=custom_key) == [('Gold', 'Bronze'), ('Silver', 'Gold'), ('Silver', 'Bronze')]
1
votes

You could create a mapper function, that takes your key function and returns another function, applying the key function to each element of some iterable.

def mapper(function):
    def inner(values):
        return tuple([function(x) for x in values])
    return inner

Example:

>>>sorted([('Gold', 2), ('Bronze', 1), ('Gold', 1)], key=mapper(custom_key))
[('Gold', 1), ('Gold', 2), ('Bronze', 1)]

Or similar, using functools.partial with map:

>>> sorted([('Gold', 2), ('Bronze', 1), ('Gold', 1)], key=functools.partial(map, custom_key))
[('Gold', 1), ('Gold', 2), ('Bronze', 1)]
1
votes

The key function can return a tuple:

medals_map = {'Gold': 1, 'Silver': 2, 'Bronze': 3 }

def custom_key(item):
    if isinstance(item, basestring):
       try:
           return medals_map[item]
       except KeyError:
           pass
    return item

print sorted([('Gold', 2), ('Bronze', 1), ('Gold', 1)], key=lambda x: (custom_key(x[0]), x[1]))

Prints [('Gold', 1), ('Gold', 2), ('Bronze', 1)]. Essentially this makes the sorting behave the same as your first example, i.e. sorted([(1, 2), (3, 1), (1, 1)]).

Edit: you can also make a custom key that returns a tuple, which may be prettier.

0
votes

You don't need a extra function. You can also do it:

medals_map = {'Gold': 1, 'Silver': 2, 'Bronze': 3 }
List = [('Bronze', 1), ('Gold', 1), ('Gold', 2)]

new_list = sorted(List, key=lambda word: (medals_map[word[0]], word[1]))

print new_list

Output:

[('Gold', 1), ('Gold', 2), ('Bronze', 1)]