Actually, I’m not sure that’s true. LATER: it’s not true
.
Here’s a list, and we already know <for sets meets the criteria for a strict partial order:
[{1, 5}, {1}, {3, 4}]
and
>>> sorted([{1, 5}, {1}, {3, 4}])
[{1}, {3, 4}, {1, 5}]
What do you think it “must” produce? That, or something else?
As far as Python is concerned, that is sorted, because a[1] < a[0] and a[2] < a[1] both return False. You may want to claim it violates stability, because it changes the relative order of the incomparable elements {1, 5} and ``{3, 4}`, but Python never intended to preserve the order of incomparable elements, and its sort has no code whatsoever to detect such a case. “Less than or not less than” are the only things it ever asks, and as few of those as it can get away with.
Now picture instead a selection sort sticking only to <. First it finds the minimum of a[:]. It starts with a[0],. {1, 5}. Then it asks whether a[1] is smaller. Yes! The new min is {1}. Then it asks whether a[2] is smaller. No. In fact they’re not comparable, but it doesn’t know that, just that a[2] < min returned False.
So it swaps the first two elements, changing the list to
[{1}, {1, 5}, {3, 4}]
Then it looks to find the minimum of a[1:]. Is a[2] < a[1]? No. So a[1] is already the minimum remaining, and the entire sort is done.
But it’s a different result than the current sort gave.
I didn’t out-think this, I just tried throwing random stuff at the current sort and an excruciatingly slow stable <-only selection sort, and was flooded with input lists that gave different results.
Why does Python produce the result it does produce? I really don’t know - slogging into the details is exceedingly tedious, and it’s always considered the lack of a total order to be a garbage-in garbage-out case. I suspect the recent change to liberalize the internal definition of “descending run” (which the example here starts with) to “non-increasing run” accounts for it. pypy, which does not yet have that change, matches the selection sort’s result.
In fact, that’s it. First
{1, 5}, {1}
is recognized as a strictly decreasing run. Then it tries to extend it for so long as it doesn’t increase. Neither of {3, 4} and {1} is < than the other, so they “must be equal”. The sort reverses them in-place, so that their original order will be restored when the entire slice is reversed at the end. Now we’re left with
[{1, 5}, {3, 4}, {1}]
and then the entire list is reversed in-place to give the final
[{1}, {3, 4}, {1, 5}]
Still just GIGO to me, though. It’s a non-decreasing order, and it did preserve the original order of the only two elements ({1} and {3, 4}) that showed a hint of being equal, given the subset of possible < compares it tried.
EDIT: this got pretty involved. Let me bottom-line the result: call
[{1, 5}, {1}, {3, 4}]
[A, B, C]
for convenience.
Python currently considers that as meeting A > B == C (the last one due to that neither B nor C is less than the other). Therefore [B, C, A] must be sorted (and preserves the order of B and C).
pypy consders that as starting with a strictly decreasing run of 2 elements, and reverses that in-place to give [B, A, C]. Then C is put into place with a binary insertion sort, whose details I didn’t bother to go into at all, but suffice it to say that C < x never returns true in this example so, it leaves C at the end.
The stable <-only selection sort ended up with the same result as pypy, but via a different chain of logic.
Not mentioned before: while pypy reflects an earlier version of CPython’s sorting code, there are plenty of cases in which it too produces different results than the selection sort. I’ll leave it to you to figure out one
:
input: [{4, 1}, {5}, {1}]
cpyhon: [{1, 4}, {5}, {1}]
pypy: [{4, 1}, {5}, {1}]
selection sort: [{1}, {4, 1}, {5}]
All sticking to <, 2 different results. BTW, while the output looks different, CPython and pypy didn’t mutate the list at all (the order in which a set’s elements are displayed isn’t defined, and {4, 1} and {1, 4} are the same set).