[Python-Dev] Decimal FAQ

Raymond Hettinger raymond.hettinger at verizon.net
Mon May 23 04:13:36 CEST 2005


Some of the private email I've received indicates a need for a decimal
FAQ that would shorten the module's learning curve.

A discussion draft follows.


Raymond


-------------------------------------------------------


Q.  It is cumbersome to type decimal.Decimal('1234.5').  Is there a way
to
minimize typing when using the interactive interpreter?

A.  Some users prefer to abbreviate the constructor to just a single
letter:

>>> D = decimal.Decimal
>>> D('1.23') + D('3.45')
Decimal("4.68")


Q.  I'm writing a fixed-point application to two decimal places.
Some inputs have many places and needed to be rounded.  Others
are not supposed to have excess digits and need to be validated.
What methods should I use?

A.  The quantize() method rounds to a fixed number of decimal
places.  If the Inexact trap is set, it is also useful for
validation:

>>> TWOPLACES = Decimal(10) ** -2
>>> # Round to two places
>>> Decimal("3.214").quantize(TWOPLACES)
Decimal("3.21")
>>> # Validate that a number does not exceed two places
>>> Decimal("3.21").quantize(TWOPLACES,
context=Context(traps=[Inexact]))
Decimal("3.21")


Q.  Once I have valid two place inputs, how do I maintain that invariant
throughout an application?

A.  Some operations like addition and subtraction automatically
preserve fixed point.  Others, like multiplication and division,
change the number of decimal places and need to be followed-up with
a quantize() step.


Q.  There are many ways to write express the same value.  The numbers
200, 200.000, 2E2, and .02E+4 all have the same value at various
precisions.
Is there a way to transform these to a single recognizable canonical
value?

A.  The normalize() method maps all equivalent values to a single
representive:

>>> values = map(Decimal, '200 200.000 2E2 .02E+4'.split())
>>> [v.normalize() for v in values]
[Decimal("2E+2"), Decimal("2E+2"), Decimal("2E+2"), Decimal("2E+2")]


Q.  Is there a way to convert a regular float to a Decimal?

A.  Yes, all binary floating point numbers can be exactly expressed as a
Decimal.  An exact conversion may take more precision than intuition
would
suggest, so trapping Inexact will signal a need for more precision:

def floatToDecimal(f):
    "Convert a floating point number to a Decimal with no loss of
information"
    # Transform (exactly) a float to a mantissa (0.5 <= abs(m) < 1.0)
and an
    # exponent.  Double the mantissa until it is an integer.  Use the
integer
    # mantissa and exponent to compute an equivalent Decimal.  If this
cannot
    # be done exactly, then retry with more precision.

    mantissa, exponent = math.frexp(f)
    while mantissa != int(mantissa):
        mantissa *= 2
        exponent -= 1
    mantissa = int(mantissa)
    oldcontext = getcontext()
    setcontext(Context(traps=[Inexact]))
    try:
        while True:
            try:
               return mantissa * Decimal(2) ** exponent
            except Inexact:
                getcontext().prec += 1
    finally:
        setcontext(oldcontext)


Q.  Why isn't the floatToDecimal() routine included in the module?

A.  There is some question about whether it is advisable to mix binary
and
decimal floating point.  Also, its use requires some care to avoid the
representation issues associated with binary floating point:

>>> floatToDecimal(1.1)
Decimal("1.100000000000000088817841970012523233890533447265625")


Q.  I have a complex calculation.  How can I make sure that I haven't
gotten
a spurious result because of insufficient precision or rounding
anomalies.

A.  The decimal module makes it easy to test results.  A best practice
is
to re-run calculations using greater precision and with various rounding
modes.  Widely differing results indicate insufficient precision,
rounding
mode issues, ill-conditioned inputs, or a numerically unstable
algorithm.


Q.  I noticed that context precision is applied to the results of
operations
but not to the inputs.  Is there anything I should watch out for when
mixing
values of different precisions?

A.  Yes.  The principle is all values are considered to be exact and so
is
the arithmetic on those values.  Only the results are rounded.  The
advantage
for inputs is that "what you type is what you get".  A disadvantage is
that
the results can look odd if you forget that the inputs haven't been
rounded:

>>> getcontext().prec = 3
>>> Decimal('3.104') + D('2.104')
Decimal("5.21")
>>> Decimal('3.104') + D('0.000') + D('2.104')
Decimal("5.20")

The solution is either to increase precision or to force rounding of
inputs
using the unary plus operation:

>>> getcontext().prec = 3
>>> +Decimal('1.23456789')
Decimal("1.23")

Alternatively, inputs can be rounded upon creation using the
Context.create_decimal() method:

>>> Context(prec=5, rounding=ROUND_DOWN).create_decimal('1.2345678')
Decimal("1.2345")


Q.  I'm writing an application that tracks measurement units along
with numeric values (for example 1.1 meters and 2.3 grams).  Is a
Decimal subclass the best approach?

A.  Like other numeric types, Decimal objects are dimensionless and
all of its methods are designed around this concept.  To add dimension,
a Decimal subclass would likely need to override every method.  For
example, without an overriding the __add__() method in a Measurement
subclass of Decimal, a calculation like "MeasurementA + MeasurementB"
would return a dimensionless Decimal object instead of another
Measurement object -- the units would be lost.

A simple alternative is to construct record tuples such as
(Decimal("1.1"),
"meters").  This allows direct use of existing decimal methods:
if a[1] == b[1]: return (a[0]+b[0], a[1]).

A more versatile approach is to create a separate class with Decimal
objects as attributes (working in a "has-a" capacity rather than an
"is-a" capacity) and then delegating the arithmetic to the Decimal
class.




More information about the Python-Dev mailing list