[Python-Dev] Symmetry arguments for API expansion

Steven D'Aprano steve at pearwood.info
Wed Mar 21 08:38:32 EDT 2018


On Wed, Mar 21, 2018 at 10:31:19AM +0000, Chris Barker wrote:
> On Wed, Mar 21, 2018 at 4:42 AM Steven D'Aprano <steve at pearwood.info> wrote:
> > > Could float et al. add an __index__ method that would return a ValueError
> > > if the value was not an integer?
> >
> > That would allow us to write things like:
> >
> > "abcdefgh"[5.0]
> >
> > which is one of the things __index__ was invented to prevent.
> 
> I’m not so sure — it was invented to prevent using e.g. 6.1 as an index,
> which int(I) would allow.

As would int(6.0). If we wanted 6.0 to be accepted as an index, then 
floats would already have an __index__ method :-)


[...]
> But Guidos point is well taken — Having __index__ fail based on value is
> setting people up for bugs down the line.
> 
> However, it seems use of is_integer() on a float is setting people up for
> exactly the same sorts of bugs.

I don't think so. You aren't going to stop people from testing whether a 
float is an integer. (And why should you? It isn't *wrong* to do so. 
Some floats simply are integer valued.) All you will do is force them to 
write code which is even worse than what they have now.

One wrong solution:

    int(x) == x

That can raise ValueError and OverflowError, but at least it is somewhat 
understandable.

Serhiy suggested that people should use the cryptic:

    (not x % 1.0)

but that's hardly self-documenting: its not obvious what it does or how 
it works. Suppose I see that snippet in a code review, and let's 
suppose I recognise it and aren't totally perplexed by it. Will it 
pass the review? I have to make a decision:

- will it fail when x is an INF or NAN?

- does it give the correct results when x is negative?

- does this suffer from rounding errors that could affect the result?

- what if x is not a float, but a Decimal, a Fraction or an int too 
  big to convert to a float?


None of the answers are obvious at a glance.

In fact, Serhiy's suggestion is not correct when x is not a float:

py> from fractions import Fraction
py> x =Fraction(1) + Fraction(1, 10**500)  # certainly not an integer
py> x.denominator == 1  # sadly Fraction doesn't support is_integer
False
py> not x % 1.0
True


> Another example is that pow() functions sometimes swap to an exact
> > algorithm if the power is an int. There's no particular reason why
> > x**n and x**n.0 ought to be different, but they are:
> >
> > py> 123**10
> > 792594609605189126649
> >
> > py> 123**10.0
> > 7.925946096051892e+20
> 
> 
> I think this is exactly like the __index__ use case. If the exponent is a
> literal, use what you mean.

Naturally. I already eluded to that in my earlier post. Nevertheless, 
this is just an example, and we shouldn't expect that the power will be 
a literal. I'm just illustrating the concept.


> If the exponent is a computed float, then you
> really don’t want a different result depending on whether the computed
> value is exactly an integer or one ULP off.

I don't think you actually mean to say that. I'm pretty sure that we 
*do* want different results if the exponent differs from an integer by 
one ULP. After all, that's what happens now:

py> x = 25
py> x**1.0
25.0
py> x**(1.0+(2**-52))  # one ULP above
25.000000000000018
py> x**(1.0-(2**-53))  # one ULP below
24.99999999999999


I don't want to change the behaviour of pow(), but we shouldn't dismiss 
the possibility of some other numeric function wanting to treat values 
N.0 and N the same. Let's say, an is_prime(x) function that supports 
floats as well as ints:

    is_prime(3.0)  # True
    is_prime(3.00001)  # False

If the argument x.is_integer() returns True, then we convert to an int 
and test for primality. If not, then it's definitely not prime.


> The user should check/convert to an integer with a method appropriate to
> the problem at hand.

Oh, you mean something like x.is_integer()? I agree!

*wink*



> If it wasn’t too heavyweight, it might be nice to have some sort of flag on
> floats indicating whether they really ARE an integer, rather than happen to
> be:
> 
> -Created from an integer literal
> - created from an integer object
> - result of floor(), ceil() or round()

I don't understand this.

You seem to be saying that none of the following are "really" integer 
valued:

float(10)
floor(10.1)
ceil(10.1)
round(10.1)

If they're not all exactly equal to the integer 10, what on earth should 
they equal?



-- 
Steve


More information about the Python-Dev mailing list