How to sort a mixed-typed list?

Viewed 104

I have a list that looks as follows (the 'None' in the list is a string, not a None):

profit = [1  , 20 , 3  , 5  , 90 , 'None', 900, 67 , 'None'] 
name   = ['a', 'b', 'c', 'e', 'd', 'f'   , 'g', 'k', 'pp']

The profit list is a list of "profit" values, so I had to sort it in a reversed order so that the highest values will be at the beginning.

Also, I have more lists with the same length that represent other things that are connected to the profit list (for example the name list that shows where the profit came from).

Now, I wrote the following code to sort the profit list in a reversed order, and I save the indices so I could sort the other lists (like name) according to the obtained indices:

sorted_ind = sorted(range(len(profit)), key=lambda k: profit[k], reverse=True)
for i in sorted_ind:
    print('{0:^50}|{1:^7}|'.format(profit[i], name[I]))

The above code works great when my profit list contains only numbers. However, there are cases where I have no profit and I would like this value to be 'None' (I don't want to set it to 0).

I'm trying to perform the same sort but in a way that all of the None indices will be at the end of the list - to sort all the ints and insert the Nones at the end.

Any good way to do it?

2 Answers

You can use a "priority" key to the sort function which checks the types as well:

>>> sorted(profit, key=lambda x: (isinstance(x, int), x), reverse=True)
[900, 90, 67, 20, 5, 3, 1, 'None', 'None']

If you're doing that just to sort the names list, then it is not necessary to sort the indices, you should use zip:

profits = [1, 20, 3, 5, 90, 'None', 900, 67, 'None']
names = ['a', 'b', 'c', 'e', 'd', 'f', 'g', 'k', 'pp']

sorted_ind = sorted(zip(profits, names), key=lambda tup: (isinstance(tup[0], int), tup[0]), reverse=True)
for profit, name in sorted_ind:
    print('{0:^50}|{1:^7}|'.format(profit, name))

Gives:

                       900                        |   g   |
                        90                        |   d   |
                        67                        |   k   |
                        20                        |   b   |
                        5                         |   e   |
                        3                         |   c   |
                        1                         |   a   |
                       None                       |   f   |
                       None                       |  pp   |

There's nothing magical about "lambda": it's just a way of spelling a simple Python function without giving it a name. With a fancier function, you could check the value at index k and use 0 for sorting instead if it happens to be "None". Which can be squashed into a "lambda" but is clearer if you use a regular function:

def keyfunc(i):
    val = profit[i]
    return 0 if val == "None" else val

So add that, and pass key=keyfunc. Done! :-)

CAUTION

If you use a unique value to indicate "don't care" (your question used the string "None", but you probably really want the Python special value None, or even math.nan instead), then a key function is - as above - easy to write with no surprises. And instead of returning 0 for "don't care", you probably want to return -math.inf instead.

But if you check on type instead (as a different answer suggested), the code becomes more delicate. For example, if you ever have a floating-point profit (like 13.75), checking for type int will treat it as "don't care" instead. Type checks are broad and brittle. There are quite a few numeric types that can be meaningfully compared to ints.

So it's generally better (more robust and clearer) to use a special value instead.

Related