[Edu-sig] <FUN>more math with Python</FUN>

Tim Peters tim.one@home.com
Mon, 1 Oct 2001 19:14:05 -0400


[Tim]
>> If the students are advanced enough, it's an interesting exercise
>> to find the smallest n for which the limitations of floating-point
>> arithmetic cause round(n!/e) to return a different result than
>> your exact computation using rationals.

[Kirby Urner]
> Not following this.
>
>    >>> def altdrs(n):
>             return round(fact(n)/e)
>
> returns the same answer as drs(n) right down to n=1.

Then 1 certainly isn't the smallest n for which equality fails <wink>.
Think about larger n, not smaller.  After all, "e" there has at best 53 bits
of precision, and for large enough n "fact(n)" can't even be represented
approximately as a float (in 2.2a4 you'll get an OverflowError then; in
earlier Pythons *probably* a platform floating Infinity).

> A related question (that I understand) would be how far
> do we need to push the series in brackets to get 1/e
> within the limitations of floating point arithmetic:

Yes, that is related.

> ...
>            while float(v) <> phi:

Note that loops like this may never terminate:  there's really no a priori
guarantee that any approximation will *exactly* equal the value computed by
some other approximation (and math.sqrt() and math.e are approximations
too).

>>     f = 0
>>     sign = 1
>>     for t in range(n+1):
>>         f += sign * F(1, fact(t))
>>         sign = -sign
>>     return fact(n) * f
>>
>> As a *programming* issue, it's thus interesting to consider whether
>> F.__mul__ should or shouldn't try to recognize the special cases of
>> multiplication by 1 and -1.  There's isn't a "one size fits all"
>> answer, so it's not for students with low tolerance for uncertainty
>> <wink>.

> Not following again.  Like, of course it should, right?
>
>    >>> f = F(1,2)
>    >>> f
>    (1/2)
>    >>> f*1
>    (1/2)
>    >>> f*-1
>    (-1/2)
>
> What other behavior would I want?

It's a pragmatic ("programming") issue, not a semantic issue:  the purpose
of special-casing 1 and -1 within F.__mul__ would not be to deliver a
different answer, but to return the correct answer *faster*.  Even just
making a copy of an unbounded rational number can take an unbounded amount
of time (depending on how long it takes to copy over all the data), so
special-casing multiplication by 1 to simply return "self" can save an
unbounded amount of runtime.

On the other hand, testing for 1 also takes time, and that time is wasted
whenver the outcome is "nope -- it's not 1".  So deciding whether it's a
good idea *overall* is a balancing act.

For most programs using floats or (machine) ints, it turns out it's usually
an overall speed loss to special-case potentially special values.  The
tradeoffs are much muddier for unbounded numeric types, while the existence
of idioms like "multiply by 1 or -1 on alternate iterations" make it an
important pragmatic issue ("who in their right mind would bother multiplying
by one?" turns out to be a poor argument <wink>).