[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