Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
tfawcett opened this issue Aug 23, 2018 · 14 comments
Closed

Comments

@tfawcett
Copy link

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.

@mattip
Copy link
Member

mattip commented Aug 24, 2018

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

@tfawcett
Copy link
Author

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

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

@charris
Copy link
Member

charris commented Aug 24, 2018

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.

@mhvk
Copy link
Contributor

mhvk commented Aug 26, 2018

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).

@eric-wieser
Copy link
Member

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.

@tfawcett
Copy link
Author

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

@charris
Copy link
Member

charris commented Aug 26, 2018

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.

@tfawcett
Copy link
Author

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.

@miccoli
Copy link
Contributor

miccoli commented Sep 28, 2018

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.

@miccoli
Copy link
Contributor

miccoli commented Jan 10, 2019

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.

@nyanpasu64
Copy link

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.

deix added a commit to Rolf-Hempel/PlanetarySystemStacker that referenced this issue Jun 11, 2019
https://docs.python.org/3/library/functions.html#round

If ndigits is omitted or is None, it returns the nearest integer to its input.

Otherwise keep int() due to:
numpy/numpy#11810
@quazgar
Copy link

quazgar commented Oct 24, 2020

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)))

@seberg
Copy link
Member

seberg commented Oct 24, 2020

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).

@seberg seberg closed this as completed Oct 24, 2020
@quazgar
Copy link

quazgar commented Oct 24, 2020

Maybe I should pull it with pip then. 😀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants