[Python-ideas] numerical type combining integer and float/decimal properties [Was: Re: Python Numbers as Human Concept Decimal System]

Guido van Rossum guido at python.org
Wed Mar 12 16:37:52 CET 2014


This representation makes more sense, it is fixed point. But you can just
use a single integer and keep track of where the point should be.
On Mar 12, 2014 8:34 AM, "Wolfgang Maier" <
wolfgang.maier at biologie.uni-freiburg.de> wrote:

> Wolfgang Maier <wolfgang.maier at ...> writes:
>
> >
> > Am Montag, 10. März 2014 14:53:42 UTC+1 schrieb Stefan Krah:
> >
> > > [My apologies for being terse, I don't have much time to follow this
> > discussion right now.]
> >
> > > Nick Coghlan <ncoghlan <at> gmail.com> wrote:
> > >> I think users of decimal literals will just need to deal with the
> risk of
> > unexpected rounding, as the alternatives are even more problematic.
> >
> > > That is why I think we should seriously consider moving to IEEE
> semantics
> > for a decimal literal.  Among other things:
> > > - Always round the literal inputs.
> > > - Supply IEEE contexts.
> > > - Make Decimal64 the default.
> > > - Add the missing rounding modes to sqrt() and exp().
> > > - Keep the ability to create exact Decimals through the constructor
> when
> > no context is passed.
> > > - Make the Decimal constructor use the context for rounding if it is
> passed.
> > > - ...
> >
> > While I find this discussion about decimal literals extremely
> interesting,
> > in my opinion, such a literal should have an underlying completely new
> > numerical type, if it is really supposed to be for inexperienced users.
> >
> > Most of the discussions right now concern rounding issues that occur
> after
> > the decimal point, but I think an at least equally big problem is
> rounding
> > *to the left* of it as in (using current float):
> >
> > >>> 1e50 + 1000
> > 1e+50
> >
> > Importantly, Decimal is no cure here:
> >
> > >>> Decimal(10**50) + Decimal(100)
> > Decimal('1.000000000000000000000000000E+50')
> >
> > (of course, you can debate context settings to make this particular
> example
> > work, but, in general, it happens with big enough numbers.)
> >
> > The solution for this example is using ints of course:
> >
> > >>> 10**50 + 100
> > 100000000000000000000000000000000000000000000000100
> >
> > , but obviously this works only for whole numbers, so there currently is
> no
> > built-in way to make this example work correctly:
> >
> > >>> 10**50 - 9999999999999999.5
> > 1e+50
> >
> > (or with Decimal:
> > >>> Decimal(10**50) - Decimal('9999999999999999.5')
> > Decimal('1.000000000000000000000000000E+50')
> > ).
> >
> > If we are discussing a new number literal I would like to have it cope
> with
> > this and my suggestion is a new numeric type that's sort of a hybrid
> between
> > int and either Decimal or float, i.e., a type that behaves like int for
> > digits left of the decimal point, but may truncate to the right.
> > In pure Python, something similar (but not a literal form of course)
> could
> > be implemented as a class that stores the left digits as an int and the
> > right digits as a float/Decimal internally. Calculations involving this
> > class would be slow due to the constant need of shifting digits between
> the
> > integer´and the float part, and might be too slow for many users even
> when
> > written in C, but its behavior would meet the expectation of
> inexperienced
> > people better than the existing types.
> > Going back to Mark Harris' initial proposal of unifying numeric types
> (which
> > I am not trying to support in any way here), such a type would even
> allow to
> > unify int and float since an ints could be considered a subset of the new
> > type with a fractional part of zero.
> >
>
> So, I've tried my hands on the pure Python implementation of this (code
> below) to make it easier for people to play around with this idea. I found
> it easier in the end to represent both the integral and the fractional part
> of the new type as Python ints (and restricting the precision for the
> fractional component).
> In the compound object the integral part has arbitrary precision, the
> fractional part a fixed precision of 28 digits by default (and in response
> to Guido: this is constant even with leading zeros), but this can be
> changed
> temporarily through a context manager (a very simple one though).
>
> This is a very preliminary version that is incomplete still and could
> certainly be optimized in many places. It's also slow, though maybe not as
> slow as I thought (it's definitely fun to work with it on simple problems
> in
> interactive mode).
>
> Feedback is welcome,
> Wolfgang
>
> Here's the code:
>
> """
> Test of a new numeric class PyNumber.
>
> Usage:
>
> Generate a new PyNumber with a fractional component of 0 (equal to an int):
>
> >>> PyNumber(3)
> PyNumber('3.0')
>
> Generate a PyNumber with a fractional component Decimal('0.1'):
>
> >>> PyNumber(3, Decimal('0.1'))
> PyNumber('3.1')
>
> Generate the same PyNumber by specifying fractional as int and exponent:
>
> >>> PyNumber(3, 1, exp = 1)
> PyNumber('3.1')
>
> Generate the same PyNumber again using a string:
>
> >>> PyNumber('3.1')
> PyNumber('3.1')
>
> Control decimal precision through context manager (default precision is
> 28):
>
> >>> with Prec(3):
>     PyNumber('0.333333333333333')
> PyNumber('0.333')
>
> >>> with Prec(7):
>     PyNumber('1') / PyNumber('3')
> PyNumber('0.3333333')
>
> Addition, subtraction, multiplication and division work as expected, but
> currently only between PyNumbers.
>
> """
>
> from decimal import *
> from math import log, floor
>
> max_exp = 28
>
> class Prec (object):
>     def __init__ (self, prec):
>         self.prec = prec
>
>     def __enter__ (self):
>         global max_exp
>         self.old_prec = max_exp
>         max_exp = self.prec
>
>     def __exit__ (self, *_):
>         global max_exp
>         max_exp = self.old_prec
>
> class PyNumber (object):
>     """Numeric type for intuitive calculations.
>
>     Supports arbitrary precision of integral component, i.e., left of the
>     decimal point, and provides fixed precision (module default: 28 digits)
>     for the fractional component, i.e. right of the decimal point.
>
>     Behaves well with calculations involving huge and small numbers, for
> which
>     float and Decimal produce rounding errors.
>
>     Modified sum example from statistics module:
>
>     >>> sum([1e50, 1.1, -1e50] * 1000) # floating point calculation gives
> zero.
>     0.0
>
>     >>> sum([PyNumber(10**50), PyNumber('1.1'), PyNumber(-10**50)] * 1000,
> PyNumber(0))
>     PyNumber('1100.0')
>
>     """
>
>     def __init__ (self, integral, fractional = 0, exp = 0):
>         # PyNumber objects are composed of two integers, one unbounded for
>         # representing the integral part of a number, the other restricted
> to
>         # max_exp digits for representing the fractional component.
>
>         if isinstance(integral, int):
>             if integral >= 0:
>                 self.integral = integral
>                 self.sign = 1
>             else:
>                 self.integral = -integral
>                 self.sign = -1
>             if fractional < 0:
>                 raise TypeError ('negative fraction not allowed')
>             if isinstance(fractional, int):
>                 self.fractional = fractional
>                 if fractional > 0:
>                     if exp == 0:
>                         self.exp = floor(log(fractional, 10)+1)
>                     else:
>                         self.exp = exp
>                 elif fractional == 0:
>                     self.exp = 0
>                 else:
>                     raise TypeError ('Unknown error caused by fractional.')
>             elif isinstance(fractional, Decimal):
>                 if fractional >= 1:
>                     raise ValueError ('need a fraction < 1.')
>                 sign, digits, exp = fractional.as_tuple()
>                 self.fractional = 0
>                 for digit in digits:
>                     self.fractional = self.fractional*10 + digit
>                 self.exp = -exp
>             else:
>                 raise TypeError ('Expected int or Decimal for fractional
> part.')
>         elif isinstance(integral, str):
>             x = integral.split('.')
>             if len(x) > 2:
>                 raise ValueError('Float format string expected.')
>             self.integral = int(x[0])
>             if self.integral >= 0:
>                 self.sign = 1
>             else:
>                 self.integral = -self.integral
>                 self.sign = -1
>             if len(x) == 2:
>                 self.fractional = int(x[1])
>             else:
>                 self.fractional = 0
>             if self.fractional > 0:
>                 self.exp = len(x[1])
>             else:
>                 self.exp = 0
>         else:
>             raise TypeError('Expected int or str as first argument.')
>         if self.exp > max_exp:
>             self.fractional //= 10**(self.exp-max_exp)
>             self.exp = max_exp
>
>     def __add__ (self, other):
>         if self.sign == -1:
>             if other.sign == 1:
>                 # -1 + 2 = 2 - 1
>                 return PyNumber(*other._sub_absolute(self))
>         if other.sign == -1:
>             if self.sign == 1:
>                 # 1 + (-2) = 1 - 2
>                 return PyNumber(*self._sub_absolute(other))
>         if self.exp >= other.exp:
>             integral, fractional = self._add_absolute(other)
>         else:
>             integral, fractional = other._add_absolute(self)
>         return PyNumber(self.sign * integral, fractional, self.exp)
>
>     def _add_absolute(self, other):
>         # add integral components.
>         integral = self.integral + other.integral
>         # adjust and add fractional components
>         fractional = self.fractional + other.fractional *
> 10**(self.exp-other.exp)
>         # determine overflow of fractional sum into integral part
>         overflow = fractional // 10**self.exp
>         if overflow:
>             integral += overflow
>             fractional -= overflow * 10**self.exp
>         return integral, fractional
>
>     def __sub__ (self, other):
>         if self.sign == -1 and other.sign == 1:
>             # -1 - 2 = -(1 + 2)
>             integral, fractional = self._add_absolute(other)
>             return PyNumber(-integral, fractional, self.exp)
>         if other.sign == -1 and self.sign == 1:
>             # 1 - (-2) = 1 + 2
>             return PyNumber(*self._add_absolute(other), exp = self.exp)
>         if other.sign == -1 and self.sign == -1:
>             # -1 - (-2) = 2 - 1
>             return PyNumber(*other._sub_absolute(self))
>         return PyNumber(*self._sub_absolute(other))
>
>     def _sub_absolute (self, other):
>         sign = 1
>         # merge integral and fractional components into big ints.
>         a = self.integral * 10**self.exp + self.fractional
>         b = other.integral * 10**other.exp + other.fractional
>         # adjust the two representations.
>         if self.exp > other.exp:
>             b *= 10**(self.exp-other.exp)
>         elif other.exp > self.exp:
>             a *= 10**(other.exp-self.exp)
>         # subtract.
>         diff = a-b
>         if diff < 0:
>             diff = -diff
>             sign = -1
>         # split the big int back into integral and fractional.
>         shift = max(self.exp, other.exp)
>         integral, fractional = divmod(diff, 10**shift)
>         return sign * integral, fractional, shift
>
>     def __mul__ (self, other):
>         if self.exp < other.exp:
>             return other * self
>         # multiply the integral components.
>         p1 = self.integral * other.integral
>         # adjust the fractional parts.
>         f1 = self.fractional
>         f2 = other.fractional * 10**(self.exp-other.exp)
>         # cross-multiply integral and fractional components.
>         p2 = (
>             self.integral * f2 * 10**self.exp +
>             other.integral * f1 * 10**self.exp +
>             f1 * f2)
>         # determine overflow of cross-multiplication into integral part.
>         shift = 2 * self.exp
>         overflow = p2 // 10**shift
>         if overflow > 0:
>             p1 += overflow
>             p2 -= overflow * 10**shift
>         # truncate trailing zeros.
>         while not p2 % 10:
>             p2 //= 10
>             shift -= 1
>         sign = self.sign * other.sign
>         return PyNumber(sign*p1, p2, shift)
>
>     def __truediv__ (self, other):
>         # merge integral and fractional components into big ints.
>         n = self.integral * 10**self.exp + self.fractional
>         d = other.integral * 10**other.exp + other.fractional
>         # adjust the two representations.
>         if self.exp > other.exp:
>             d *= 10**(self.exp-other.exp)
>         elif other.exp > self.exp:
>             n *= 10**(other.exp-self.exp)
>         # divide.
>         integral, remainder = divmod(n, d)
>         # turn remainder into fractional component.
>         remainder *= 10
>         shift = 1
>         while remainder % d and shift < max_exp:
>             remainder *= 10
>             shift += 1
>         fractional = remainder // d
>         sign = self.sign * other.sign
>         return PyNumber(sign * integral, fractional, shift)
>
>     def __int__ (self):
>         return self.integral
>
>     def __float__ (self):
>         return self.sign * (self.integral + self.fractional / 10**self.exp)
>
>     def __repr__ (self):
>         return "PyNumber('{0}.{1}{2}')".format(self.sign * self.integral,
> '0'*(self.exp-len(str(self.fractional))), self.fractional)
>
>
> _______________________________________________
> Python-ideas mailing list
> Python-ideas at python.org
> https://mail.python.org/mailman/listinfo/python-ideas
> Code of Conduct: http://python.org/psf/codeofconduct/
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20140312/b71cbc99/attachment-0001.html>


More information about the Python-ideas mailing list