Numpy: round() returns floating point, not int, for some numpy floats when no second arg

Created on 23 Aug 2018  ·  14Comments  ·  Source: numpy/numpy

The semantics of round() changed in Python 3:

round(number[, ndigits])
Return number rounded to ndigits precision after the decimal point. If ndigits is omitted or is None, it returns the nearest integer to its input.

This works incorrectly in the case of np.float64, which returns a float. I believe the __round__ method is calling __rint__, which should return an integer but doesn't.

Reproducing code example:

import numpy as np
In [50]: round(1.0)
Out[50]: 1

In [51]: round(float(1.0))
Out[51]: 1

In [52]: round(np.float(1.0))
Out[52]: 1

In [53]: round(np.float64(1.0))
Out[53]: 1.0

This behavior is the same for float16, float32, and float128.

Most helpful comment

Another thought on this issue: since isinstance(np.float64(1), float) is true, the current implementation breaks Liskov substitution principle making the use of numpy scalars very unSOLID.

The problem is that one has a lot of paths (sometimes unexpected) in which numpy.float64 values sneaks into existing code, which makes unit testing and maintenance unnecessarily cumbersome.

All 14 comments

what type is being returned? np.float is the same as float, np.float64 returns a numpy scalar:

>>> type(np.float(1.0))
<class 'float'>
>>> type(np.float64(1.0))
<class 'numpy.float64'>

Your examples with np.float are not using numpy. The call to round(np.float64(1)) actually goes to np.round, the documentation states: (actually documented in np.around) "returns an array of the same type)" so if you check the type of Out[53] you will see it is a np.float64

what type is being returned? np.float is the same as float, np.float64 returns a numpy scalar:

type(np.float(1.0))

type(np.float64(1.0))

Your examples with np.float are not using numpy. The call to round(np.float64(1)) actually goes to np.round, the documentation states: (actually documented in np.around) "returns an array of the same type)" so if you check the type of Out[53] you will see it is a np.float64

You're explaining what the code does. I have to agree: yes, that's what it does.

NumPy round applied to numpy floats does not return integers. It is a feature, not a bug. The Python behavior you illustrate is new in Python 3.

Just to elaborate a little more: the problem is with very large numbers; in python, one can return a long integer, but in numpy we cannot (for the general case of arrays). We have had some discussion, however, whether this should change at least for __round__, i.e., if one does python's round(array).

NumPy round applied to numpy floats does not return integers. It is a feature, not a bug.

@charris: I don't think we're talking about np.round here, but the other round. I'd consider not complying with the api of round a bug, but I suspect it's already reported elsewhere on github.

I get the situation: Python's round() delegates responsibility to np.__round__, which in turn calls np.round(), which doesn't obey the semantics of Python's round(). And there's no reason it should, especially since (as you say) round's behavior is new with Python 3. It would be nice if np.__round__ checked its second argument and called np.rint when it is zero, so it conformed to Python round's new semantics, but I can understand if there are reasons you don't want to do that.

It's just confusing to have code like:
mylist = [0] * round(x + y)
which works most of the time but gives a confusing message when x or y is taken from a numpy structure. But numpy's datatypes are not Python's, and there we are.

Regards,
-Tom

After we drop Python 2.7 we might want to take a second look at this. However, backwards compatibility is always a consideration. Although in this case I expect people do want an integer, especially for indexing. There has been a similar discussion about ceil and floor. One option would be to make new functions, iround, iceil, and ifloor, although deciding the return type might be problematic with either np.intp or np.int64 being possibilities.

I stand corrected---np.rint returns an rounded integer value of the type passed, so calling it wouldn't fix anything. Maybe, since Python's round function has changed its semantics, it should be round's responsibility to do any conversion necessary to guarantee those semantics. Oh well.

From PEP3141:

@abstractmethod
    def __round__(self, ndigits:Integral=None):
        """Rounds self to ndigits decimal places, defaulting to 0.

        If ndigits is omitted or None, returns an Integral,
        otherwise returns a Real, preferably of the same type as
        self. Types may choose which direction to round half. For
        example, float rounds half toward even.

        """
        raise NotImplementedError

A quick test (Python 3.7.0) shows:

>>> import numpy as np; print(np.__version__)
1.15.2
>>> 1e+24.__round__(None)
999999999999999983222784
>>> 1e+24.__round__(0)
1e+24
>>> 1e+24.__round__()
999999999999999983222784
>>> np.float64(1e24).__round__(None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: an integer is required (got type NoneType)
>>> np.float64(1e24).__round__(0)
1e+24
>>> np.float64(1e24).__round__()
1e+24

If I got it right: current __round__ implementation is not PEP3141 compliant, since np.float64.__round__ does not allows NoneType for the ndigits argument, and defaults its value to 0 and not None when called without arguments.

I think that it would be sensible to adhere immediately to the PEP3141 calling signature. When np.float64.__round__ is called with ndigits=None I would suggest to alert the user that the result is not Python 3 compliant, by either

  • raising NotImplementedError (hard option)
  • raising a UserWarning (soft option).

EDIT:

I just noticed that this has already been discussed in #11557, #5700, #3511. Sorry for adding noise to the discussion, but I feel that a ref to PEP3141 is important.

Another thought on this issue: since isinstance(np.float64(1), float) is true, the current implementation breaks Liskov substitution principle making the use of numpy scalars very unSOLID.

The problem is that one has a lot of paths (sometimes unexpected) in which numpy.float64 values sneaks into existing code, which makes unit testing and maintenance unnecessarily cumbersome.

I've encountered this issue as well. At least once my own code has broken since round(np.int32 / float) == np.float64 which cannot be used for array dimensions/etc.

I've encountered this issue as well. At least once my own code has broken since round(np.int32 / float) == np.float64 which cannot be used for array dimensions/etc.

Workaround for me:

width = round(float(np.sqrt(x)))

Hey Daniel :). The workaround is good, closing the issue since this will now return a python integer for version NumPy 1.19 and later (fixed in gh-15840).

Maybe I should pull it with pip then. :grinning:

Was this page helpful?
0 / 5 - 0 ratings