This is the last post I hope to make about semantics. Following numpy is almost always the best idea when adding a new number-crunching function to Python, and I don’t think this is an exception, except for Python’s own Decimal type.
- For numpy’s own floating types, it returns numpy floats, including passing through NaNs, but losing the sign bit of 0.
- But @mdickinson pointed to a
numpy issue suggesting they’ll be changing to preserve a zero’s sign. We should do that from the start then.
- For types other than numpy floats, it does not appear ever to convert to its own float types. Instead it returns ints in {-1, 0, 1}.
Which it appears to do by “the obvious” compare-to-int-0 method.
>>> import numpy
>>> import datetime
>>> numpy.sign(datetime.timedelta(0))
Traceback (most recent call last):
...
TypeError: '<' not supported between instances of 'datetime.timedelta' and 'int'
For floating types it doesn’t know about, that usually words fine too, but the result is not a float:
>>> import mpmath
>>> numpy.sign(mpmath.mpf(-3.1))
-1
>>> type(_)
<class 'int'>
>>> import gmpy2
>>> numpy.sign(gmpy2.mpfr(-3.1))
-1
>>> type(_)
<class 'int'>
>>> import decimal
>>> numpy.sign(decimaL.Decimal(-3.1))
-1
>>> type(_)
<class 'int'>
For NaNs of floating types it doesn’t know about, it raises an exception, presumably by detecting that trichotomy fails on its attempts to compare with int 0:
>>> numpy.sign(gmpy2.mpfr("nan"))
Traceback (most recent call last):
...
TypeError: unorderable types for comparison
But it’s worse for Decimal, because Python’s Decimal deviates from IEEE-754 by enabling the invalid operation trap by default (and trying to do an ordered compare with a NaN IS an invalid operation by the standard’s rules):
>>> numpy.sign(decimal.Decimal("nan"))
Traceback (most recent call last):
..
numpy.sign(decimal.Decimal("nan"))
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]
For Python’s other numeric types (ints and Fraction), it “just works”, and also for many other package’s numeric types (e.g. gmpy2.mpq, its rational type).
Python’s version should certainly work seamlessly with Python’s own Decimal type, but other than that I’m perfectly happy with what numpy does in all cases. I don’t even mind enough about timedelta failing to complain.
WRT Decimal, the “compare” method almost does the whole job by itself (just losing the sign of 0), including not raising InvalidOperation for comparing a NaN:
>>> for base in -2, 0, 4, "nan":
... X = decimal.Decimal(base)
... r = X.compare(0)
... print(X, r, type(r))
-2 -1 <class 'decimal.Decimal'>
0 0 <class 'decimal.Decimal'>
4 1 <class 'decimal.Decimal'>
NaN NaN <class 'decimal.Decimal'>
I don’t see any good reason for why Python should be less capable than numpy here, nor any particularly good reason for why it should try to be more capable. If you have a sane type that can be compared to int 0, it will “just work”. Else it’s garbage in, garbage out. Use a sign implementation for that type’s oddball idea of “sign”.
The solution to the real but apparently very rare “well, it would work if you coerced to float first” is obvious: apply float() yourself before calling math.sign(). Then if unanticipated overflow or underflow gives you a bad outcome[, it’s entirely on you too. You asked for it, the implementation didn’t force it on you.
And now I’ll make a note to check in again here after another year passes
.