Include `math.sign`

I agree that there is a trade-off between

  1. providing a “batteries-included” function that has some defined behavior that has been discussed in detail by some subset of users (the people in this discussion now)

and

  1. Not stifling user liberty in defining their own exception handlng.

That said, There are 3 edge cases to handle: +0, -0, and nan (4 if you count ±nan as separate cases). I argue that the numpy handling of these cases is the best choice that can be made in a math oriented module.

  • Mathematically there is no difference between +0 and -0 so it is ok if they resolve to the same result.
  • Mathematically nan is not a number so it dosn’t make sense for it to have a sign. The sign of nan is not a number, it is nan.
  • The difference between ±nan has nothing to do with mathematics again, so this difference doesn’t need to be resolved by a math function.

Finally, even though users can’t resolve the difference between +0 and -0 or +nan and -nan, they CAN resolve if a 0 edge case was hit (the output is 0.0) and they can resolve when a nan edge case is hit (the output is nan). So if they need different error handling in these cases then they can write their own wrapper to catch these cases. That is not, for example, the case with sign = lambda x: copysign(1, x).

To recap: The mathematical sgn function need not distinguish ±0 and ±nan, and if users need to distinguish the 0 and nan edge cases differently then returning 0 and nan then users are not prevented from wrapping sign and providing their own edge case handling.

If users need to resolve the difference between +0 and -0 or +nan and -nan (that is, they need access to the payload of the float) then I argue they should be looking to another module than math, because those distinction are not mathematical distinctions, but rather float implementation details. A module more oriented towards bytes investigation might be appropriate. That said… users can already get access to the payload within the math module using math.copysign. I would say this could be the distinction between sign and copysign. sign implements the mathematical signum operation while copysign provides access to the sign bit of the underlying float.


The suggestion to write a utility code example in the docs and point to that is a fair one and could resolve the confusion if it is easily discoverable. I would argue that copysign is not that discoverable to a newcomer. They are looking for a function called sign or sgn that starts with an s, not c. So it would at least be nice if the discoverability of the utility example is addressed. But this seems possible.

For my purposes so far performance is not critical. If I was worried about performance I would use numpy. That said, I’m not sure how much care has traditionally been taken in the math module to support performant implementations. If care is typically taken then I would say the same should be done for sign and it should be investigated whether a c implementation would be faster. If performance is not typically a major concern then the python implementation is fine.

I’m also not personally worried about having sign work on large numbers, but it’s helpful to have that pointed out.

By null do you mean the python None object? I prefer nan as an explicit signal that nan input edge case was hit. Returning None is the behavior of a function which has no return value. I don’t like this choice.

If your logic is that the mathematical sign functions “don’t deal with nans” then I would continue that logic to say that sign() should raise an exception on non-numeric input. But that said, I think it is standard for mathematical functions to return nan in some cases when they encounter input they can’t handle. My understanding is part of the idea of nan is that it passes through mathematical operations propagating itself, but not raising exceptions. That is, mathematical multiplication is an operation defined on two complex numbers, it “does not deal with nans”, but nonetheless we have float("nan") * 3 returns nan.

1 Like

See my reply a few posts above where I defend numpy’s choice and argue it is a good one. There I suggested math.sign implements a more mathematical operation which doesn’t care about the distinction between ±0 and ±nan. If you want access to the payload you can use copysign or some byte investigation.

That said, I’m very curious what use case you have that has you interested in the payload of 0 or NaN.

That is just not true. You are referring to real numbers, the finite precision floating numbers -0.0 and 0.0 are mathematically different.

This is meaningless. There is no concept of “number” in mathematics. Saying that nan doesn’t belong to something undefined carries no meaning. nan is an instance of float as rightfully as any other instance.

Now, there is different comment that uses similar words that can be made. It is that the sign of nan carries no information. This doesn’t mean that it doesn’t carry a sign. It does. I means that, its sign doesn’t have meaning like the sign of -0.0 does, which carries information about the sign of the result of the operations before the rounding was applied.

I’m suggesting that the sign function treat its inputs as real numbers. This is what most users probably expect, i.e. that floats are the computers way of representing real numbers. There is no distinction between +0 and -0 on the reals and there is no nan, or +nan, or -nan on the reals, so again, the “real”-oriented signum function need not distinguish +nan or -nan. But it can distinguish when nan is passed in as opposed to a normal real number (non-nan float).

There are cases where the underlying float implementation details might be important, but I’m suggesting that this function not be the place where users can go to access those implementation details. They could go to copysign for that.

Again, I’m curious for cases where the payload information is important.


Call it the extended real line. We’ll count ± infinity.

1 Like

They are not. Some are int, some float. Thinking that the purpose of float is to be real numbers is the source of so many confusions.

There are lots of literature regarding the design decisions in IEEE 754. There is no much point repeating them here.

I am not sure the sign of 0 is also called payload. Maybe. There is a wiki page talking about that particular.

The math module contains plenty of functions that are designed to support people explicitly interested in finite precision floating-point numbers - isnan, fmod, frexp, ldexp, nextafter, ulp, and arguably fsum (because the only reason it exists as a separate function than sum is because of floating point rounding issues). And isclose is designed for implementing numerical calculations, which rely on understanding the limitations of the underlying representation.

Yes, a cursory glance at the math module suggests that it’s about mathematical real numbers. But you very quickly discover that it’s just as much about concrete floating-point details.

For me, copysign (and any proposed sign function) is about floating point. For mathematical use, I’d just use comparisons:

def signum(x):
    return -1 if x < 0 else 0 if x == 0 else 1

(ref: Sign function - Wikipedia)

5 Likes

Thanks for pointing that out.

What about supporting sign and fsign?

def sign(x):
    if x > 0:
       return +1.0
    elif x < 0:
        return -1.0
    elif x == 0:
        return 0.0
    else:
        return float("nan")

def fsign(x):
    return copysign(1, x)

Or… in the interest of not adding even more to the API, supporting sign and copysign? I’m more comfortable relegating people interested in the sign bit of 0 and nan to use the more verbose copysign because they already know enough to know what a float is and what its sign bit is. They know about esoteric details of float special values, that nan even exists. The idea of math.sign would be, in part, to help beginner users who just want a simple function available that returns whether a number is positive, negative, or zero.

This is a good one-liner. It outputs 1 for nan input which might be a fine choice. Again, the most important point I’m trying to make is that the value-add from math.sign would be that it saves the user time they might lose by missing edge cases on early drafts of their sign functions or by thinking about how edge cases can be handled. That is, let’s think about the smart way to handle edge cases as a community up front, include that thinking in stdlib, then users can benefit from that forethought.


The counter-argument I’m expecting to hear is: no, it’s fine, users can figure it out and write their own functions. Maybe they can learn something along the way. We can document an example in the stdlib documentation to make it easier for users but we don’t need a function.

I think this is a valid argument, but my preference would be to include the function because that would be easiest for new (and all) users, the maintenance cost is (presumably, but I don’t know?) low, and it is, in my opinion, to be expected of a math library to provide a simple sign function.

1 Like

There is no concept of “number” in mathematics.

This is false. There are precise definitions and descriptions of various types of numbers, including real numbers and complex numbers. For an accessible definition of real numbers, for example, you can refer to ‘What is Mathematics’ by Courant and Robbins. My copy is about 25 years old, but still quite serviceable - you can refer to chapter 2 (Number System of Mathematics).

You are talking about nans being special floats, but floats are implementations of real numbers, have limited precision, and therefore not strictly real numbers, but approximations of them.

3 Likes

Raising an exception for nans would not be invalid. I see this purely as a detail of implementation.

Look, mathematician here. I meant what I said, and precisely what I said. There is no place where you will find “number” as a concept in mathematics. Simply because there is no theorem that needs it as a hypothesis. What there are are “real numbers”, “natural numbers”, “complex numbers”, etc. And there is a point at which extensions of those concepts start loosing the “number” surname, just because their numberness that fussy common language notion of what is a number is not what is important about them.
So, even trying to define “number” as the union of all the concepts that have the word “number” in their name, that wouldn’t be a precise definition. It wouldn’t serve any purpose either.

Above I explained what I meant to say (which is what I wrote, read strictly as written).

This quote from you contains a misunderstanding. floats are not “defective real numbers” or their properties just implementation details. Finite precision is a feature (sure imposed by reality). Their algebraic properties are as “mathematical” as those of the real numbers. They are studied in numerical analysis. They are just a different algebraic structure.

3 Likes

Or it might lose the user time by choosing behaviours for the edge cases which the user didn’t expect and which don’t match what the user wants.

IMO, “not every 3-line function needs to be in the stdlib” applies here.

I honestly don’t care one way or another whether this gets added to the stdlib. My point was, though, that if it does get added, it should be in a way that’s useful to people who do work with the technical details of floating point (for whom “edge cases” like signed zeroes, NaN, etc. are familiar and well-understood) because that’s the only situation where there’s any sort of complexity that makes writing your own implementation non-trivial.

New users will probably just use comparisons, and will never need this function.
The maintenance is higher than you think because of the need to support the edge cases (and handle the inevitable bug reports that require you to justify the particular behaviour you chose).
And I hardly think it’s “expected” of a math library to supply trivial functions. I’d prefer it to focus on things that are hard to implement, or which need low-level access to the underlying representation or hardware.

1 Like

Yes, users can spin their own and cover non-zero/non-nan cases easily. But I’m specifically imagining the case where someone writes their own function, they didn’t think about edge cases, but down the road callers of their code start calling the edge cases and things break.

I came to Python from Matlab and Mathematica, both of which are mathematically oriented programs that provide a sign function. It was already brought up that rust provides a sign function. It looks like java also provides a signum function. It looks like go has a sign function. Julia has a sign function. These are all just quick google searches, I don’t have experience with these languages. Not sure what other peer languages python looks at. I’ll try to compile a list of the ±0 and ±nan handling for all these different languages.

3 Likes

Floats are implementations of rational numbers with very specific denominators (powers of two).

I think that sign is not really more trivial than abs which can often be found in math libraries. Python’s math module seems to have originally been a wrapper around C’s math.h which has a fabs function but Python decided to promote that to builtin type generic abs instead.

In case it is not clear to others here sign as a function is quite common in mathematical computing environments e.g. Matlab, Mathematica, Maple, R, Julia, excel. Many Python mathematical modules have the sign function e.g. SymPy, NumPy mpmath, pytorch, tensorflow, …

If you look through these and more you can see that:

  1. In some cases sign is only for real numbers.
  2. Apart from numpy whenever it is defined for complex numbers it is z/|z|.
  3. Apparently no one documents what happens with negative zeros.
9 Likes

FYI Numpy will follow this too since they’re committed to following the Array API.

Negative zeros are explicitly documented to return zero.

All the Array API implementers have to implement this for complex numbers.

1 Like

Firstly, numbers in mathematics are always of a specific type. What is the mathematical meaning - or is there a meaning - attached to an abstract concept of number? I am not aware of any branch of number theory that deals with abstract numbers. So your point about there being an abstract concept of number seems irrelevant to this discussion.

Secondly, I have no misunderstanding about floats and reals - it is accurate to say that floats are approximations of real numbers. I did not write that floats are “defective real numbers”, those are your quotation marks.

1 Like

Nearly none of their operations satisfy the same properties. So, no. Not accurate at all.

The answer to that question is none, as I said above. The relevance is because “nan is not a number” was being used a premise or an argument. I only pointed out that that premise is vacuous. Even if you replace “number” in it by one actual concept in mathematic, like “real numbers”, still doesn’t say anything about nan since where they belong is to a different structure.

Also on the subject of trivial functions consider this. SymPy has a function lambdify which turns a symbolic expression into a Python function that can evaluate the expression using functions from different “math modules” like math, scipy, numpy, mpmath. It has to generate specific code for math because of the absence of the sign function:

In [8]: f = lambdify(x, sign(x), modules='numpy')

In [9]: ?f
...
def _lambdifygenerated(x):
    return sign(x)

In [10]: f = lambdify(x, sign(x), modules='math')

In [11]: ?f
...
def _lambdifygenerated(x):
    return (0.0 if x == 0 else copysign(1, x))

SymPy also has code printers for other languages:

In [33]: mathematica_code(sign(x))
Out[33]: 'sign[x]'

In [34]: julia_code(sign(x))
Out[34]: 'sign(x)'

In [35]: maple_code(sign(x))
Out[35]: 'sign(x)'

In [36]: octave_code(sign(x))
Out[36]: 'sign(x)'

In [37]: pycode(sign(x))
Out[37]: '(0.0 if x == 0 else math.copysign(1, x))'

SymPy needs an awkward workaround here specifically for Python because Python’s math module lacks a function that other environments typically have. Of course there are many other differences that the code printers have to navigate. There is some value though in generally having a consistent set of functions in different computing environments even if some of those functions are trivial.

5 Likes

Sorry, I didn’t mean to get the discussion sidetracked on the matter of “triviality”. I don’t personally think a sign function is that important, because it’s so easy to write a “good enough for me” version. It’s that sense in which I meant that I didn’t think maths libraries are “expected” to include one.

If one is to be added, let’s make sure it is useful to all stakeholders (and that includes people doing low-level floating point bit manipulation at least as much as people doing maths who want an x < 0 function).

I’d be interested to know what a specialist like @mdickinson thinks of this proposal. Am I making a meal out of nothing here?

1 Like