The Oddler
The Oddler

Reputation: 6718

Python: Ordering list of string and None based on other list?

I have two lists of equal length, one containing numbers, the other strings and None. I want to order them by descending numbers, keeping the other one in sync.

Before the second list could only contain string (so no None elements), and I used this code:

weights, urls = zip(*sorted(zip(unordered_weights, unordered_urls), reverse=True))

The numbers are in the "weights" array, the strings in the "urls" array. This worked fine.

However, now that I allow None in my strings list, I get the following error:

TypeError: unorderable types: str() < NoneType()

For some reason it's also trying to sort the strings, I guess in case the weights are the same. How can I fix this?

I'm using Python 3, I read that in Python 2 None came before any string, but now it gives an error. The order of strings with the same weight doesn't matter.

Upvotes: 2

Views: 432

Answers (2)

zegkljan
zegkljan

Reputation: 8411

Sorting tuples is done lexicographically - first the first elements are compared, if they are equal then the second, which in your case can be None. You can sort using only the weights by extracting it as the sorting key through the key argument to sorted():

weights, urls = zip(*sorted(zip(unordered_weights, unordered_urls), reverse=True, key=lambda x: x[0]))

Observe:

In [1]: unordered_urls = ['b', 'a', None, 'c', None]
In [2]: unordered_weights = [1, 0, 0, 5, 2]  # the 'a' and None have the same weight
In [3]: weights, urls = zip(*sorted(zip(unordered_weights, unordered_urls), reverse=True))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-61fb3631580a> in <module>()
----> 1 weights, urls = zip(*sorted(zip(unordered_weights, unordered_urls), reverse=True))

TypeError: unorderable types: str() < NoneType()

In [4]: weights, urls = zip(*sorted(zip(unordered_weights, unordered_urls), reverse=True, key=lambda x: x[0]))
In [5]: weights
Out[5]: (5, 2, 1, 0, 0)
In [6]: urls
Out[6]: ('c', None, 'b', 'a', None)

Upvotes: 5

Martijn Pieters
Martijn Pieters

Reputation: 1123450

Python will use the second element in each tuple (your string or None values) if first element is equal between two tuples:

>>> (42, 'foo') < (42, None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: str() < NoneType()

You could insert a tie-breaker:

from itertools import count

weights, __, urls = zip(*sorted(zip(unordered_weights, count(), unordered_urls), reverse=True))

The count() iterable object will insert integers starting at 0; when two weights are the same, Python will then compare those integers. Since they always differ, the elements in the 3rd position will never be compared against one another. Because the count increases monotonically, the sort remains stable otherwise, two elements with the same weight are kept in the same relative order.

Or you could tell sorted() to only look at the first element:

from operator import itemgetter
weights, urls = zip(*sorted(zip(unordered_weights, unordered_urls), 
                            reverse=True, key=itemgetter(0)))

Now two equal weights are left in the same order too, because the Python sorting algorithm (TimSort) is stable by default.

Upvotes: 4

Related Questions