The Idea
The idea is to add a sign function to the math module. The behavior for finite, non-zero floats/ints/decimals is obvious. Likewise for ± inf.
The Utility
I think the utility of a sign function is pretty obvious. It’s one of the most basic arithmetic operations. My main uses cases involve mathematically oriented code that needs to switch behavior based on the sign of some input.
Current Workarounds
The current workarounds are
- Use some combination of
x>0,x>=0,x<0,x<=0, to check for the sign ofx. This may give surprising behavior ifxis ±nan or ±0. - One can hack a more thorough
signfunction usingmath.copysign. Basicallysign(x) = copysign(1, x). This option always returns ±1.0. For ±0 it returns ±1.0 and for ±nan it returns ±1.0. - If
numpyis already a dependency one can use numpy.sign.np.signreturns 0 for ±0 inputs and returnsnanfor ±nan inputs. - There are likely others
Edge cases
There are two edge cases. ±0 and ±nan.
Ideas for how to handle the edge cases:
- Always return the result of
math.copysign(x, y). With this form ±0 returns ±1.0 respectively and ±nan also returns ±1.0 (note that historically on some systemsmath.copysign(1, float("nan"))would return-1.0and vice-versa for-nan, but on my system, Windows with Python 3.9 for the moment, the expected thing is returned). On this choice thesignfunction would always return ±1.0. - Sometimes return
0.0. For this choicesign(float("0"))andsign(float("-0"))would both return0.0. Possibly the same would hold for ±nan. - Sometimes return
nan. For this choicesign(float("nan"))andsign(float("-nan"))would both returnnan. Maybe the same would apply tosign(0) - Sometimes return
-nanfor this casesign(float("nan"))would returnnanbutsign(float("-nan"))would return-nan.
The np.sign behavior described above is a combination of the second and third behaviors. Note that np.sign accepts complex input. For this idea I would first propose that math.sign have the same behavior as np.sign for all float (and decimal) inputs but that math.sign not support complex input. However, I’m interested in discussions about alternative behaviors.
Prior Discussion
It seems this was discussed around 2007 - 2009
The first google hit links to a stackoverflow question (2009). The main answer links to a rejected patch (2007) which included math.sign and claims it was rejected because of lack of agreement on the edge cases citing this discussion (2008). The discussion specifically worries about handling of ±0.
Here is a issue (2009) showing that, for some systems and some versions of python copysign gives surprising results for nan. That is some combinations surprisingly gave
copysign(1.0, float("nan"))
# -1.0
copysign(1.0, float("-nan"))
# 1.0
While some expectedly gave
copysign(1.0, float("nan"))
# 1.0
copysign(1.0, float("-nan"))
# -1.0
On my current system and python version I get the expected behavior. I don’t know if that is because this behavior has been cleaned up in later versions of python or if it’s by luck on my system.
Here is a rejected issue (2016) requesting the repr for float("-nan") include a minus sign. The upshot of that discussion, citing stackoverflow answer (2014), was that “it is almost always a bug to attach meaning to the ‘sign bit’ of a NaN datum.” and, by this reasoning, float("-nan") does the right thing by excluding the minus sign form the repr.
Response to the previous discussion
Agreement couldn’t be reached on edge cases back in the late 2000’s, but it looks like there wasn’t much discussion. I also don’t think any of the discussion was looking at the whole picture simultaneously in the way I’m trying to do here. That is, the discussions all seem to focus one specific edge case or another. Maybe agreement could be reached now. Also, numpy was able to reach agreement, so maybe python and math can reach agreement now also.
Tangentially, I want to respond to the 2016 discussion about repr(float("-nan")) not including the minus sign. I’ll point out that repr(Decimal("-nan")) is -NaN, including the decimal point. So there is an inconsistency here that could use resolution and help resolve confusion. The separate idea here is that neither or both should include the minus sign. I’d suggest what might be most consistent would be for neither to include the minus and sign(float("nan")) and sign(float("-nan")) both return nan. This would strengthen the general stance taken by python that the sign of nan should be ignored. It will still be accessible through copysign, but it could be documented there that significance shouldn’t be attached to the that sign. Note that some of the discussion above cite official references on floats saying things about the references being ambiguous about the significance of the sign of nan, but I’m not familiar with those references.
To summarize a little bit: Yes, it’s weird that the float specification supports ±0 and kind of allows ±nan. It doesn’t feel like that strange fact should preclude having a math.sign function. It seems like a solvable problem. Rejecting math.sign because of these edge cases feels like throwing the baby out with the bathwater.
Who would maintain this?
I expect that sign would be maintained by the current maintainers of math. This “python experts” page indicates that that would be these people
mdickinson, rhettinger, stutzbach^
I have not contacted any of them directly about this suggestion yet. This is my first time suggesting this publicly.
A simple python implementation could look like
def sign(x, /):
if isnan(x):
return float("nan")
if x == 0:
return 0.0
return copysign(1, x)
Obviously typing bells and whistles etc. could be put on. Documentation would of course be required.
I hope the implementation is simple enough that the continuing maintenance burden would be low, but I don’t have personal experience with stdlib maintenance. I would volunteer to help with the initial work and documentation.
Closing
It seems a shame such a simple and expected feature was blocked by some non-critical edge cases. I fully expect the conclusion of this idea might just very quickly be: “This was discussed previously and rejected, that old decision stands”. Nonetheless, I’m curious about some renewed discussion.