Reputation: 6718
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
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
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