===============
Decimal Paper
===============
.. contents:: Contents Table
What is Decimal?
================
Decimal is a standard-library module that brings decimal arithmetic to Python.
It's new to version 2.4, but it'll be compatible with 2.3.x versions; to install decimal.py in those Python versions check the packages according to your platform [1]_ and the corresponding instructions [2]_.
In this paper I'll explain why Decimal is needed and what its actual form is. This document is not intended to replace the formal documentation (refer to it [3]_ for more precise and detailed wording, examples, etc.), but to give a general overview (with examples! [4]_) of how to use decimal.py in the hope of it to grab the reader's interest and to encourage her/him to start using it.
Why Decimal? I needed a money data type, and for that the binary floating point (the standard "float") is too inexact: For example, in float...
>>> 1.1 - 0.2
0.90000000000000013
...and in Decimal...
>>> Decimal("1.1") - Decimal("0.2")
Decimal("0.9")
"Standard" floating point
-------------------------
Most people (even programmers) are unaware that computing with binary floating point numbers entails certain unavoidable inaccuracies. Before getting into the problems, let's take a look to their structure; binary floating point numbers are made up of three components:
* The sign, which is positive or negative.
* The mantissa, which is a single-digit binary number followed by a fractional part.
* The exponent, which tells where the decimal point is located in the number represented.
For example, the number 1.25 has positive sign, a mantissa value of 1.01 (in binary), and an exponent of 0. The number 5 has the same sign and mantissa, but the exponent is 2 because the mantissa is multiplied by 4 (2 to the power of the exponent 2).
Modern systems usually provide floating-point support that conforms to a relevant standard called IEEE 754. C's double type is usually implemented as a 64-bit IEEE 754 number, which uses 52 bits for the mantissa. This means that numbers can only be specified up to 52 bits of precision. If you're trying to represent numbers whose expansion repeats endlessly, the expansion is cut off after 52 bits.
We're used to say *floating point* when talking about binary floating point. As Decimal is also floating point, putting in this document those two words without specifying the base is kind of tricky, so I'll avoid doing it.
The problem with binary float
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In decimal math, there are many numbers that can't be represented with a fixed number of decimal digits, e.g. 1/3 = 0.3333333333.......
In base 2 the same happens with numbers like decimal 0.2, which equals to the binary fractional number 0.001100110011001.... But as I said above, IEEE 754 has to chop off that infinitely repeated decimal after 52 digits, so the representation is slightly inaccurate, resulting in small roundoff errors. The inaccuracy isn't always visible when you print the number because the binary-floating-point-to-decimal-string conversion is provided by the C library, and most C libraries try to produce sensible output. Even if it's not displayed, however, the inaccuracy is still there and subsequent operations can magnify the error.
For many applications this doesn't matter. If I'm plotting points and displaying them on my monitor, the difference between 1.1 and 1.1000000000000001 is too small to be visible. Reports often limit output to a certain number of decimal places, and if you round the number to two or three or even eight decimal places, the error is never apparent. However, for applications where it does matter, it's a lot of work to implement your own custom arithmetic routines.
Towards Decimal
---------------
Instead of a binary data type, we need a decimal data type that represents exactly decimal numbers.
Why floating point?
^^^^^^^^^^^^^^^^^^^
So, we go to decimal, but why *floating point*?
The opposit to a floating point number is a fixed point number, where the position of the decimal point is fixed. For a fixed point data type, check Tim Peter's FixedPoint at SourceForge [5]_. The standard in which Decimal is based specifies it as floating point, but a fixed point data type can be built over it.
A floating point number, as explained before, uses a fixed quantity of digits (precision) to represent a number, working with an exponent when the number gets too big or too small. In contrast, we have the example of a ``long`` integer with infinite precision, meaning that you can have the number as big as you want, and you'll never lose any information.
But why can't we have a floating point number with infinite precision? It's not so easy, because of inexact divisions; e.g.: 1/3 = 0.3333333333333... ad infinitum. In this case you should store a infinite amount of 3s, which takes too much memory, ;).
With that said maybe you're thinking "Hey! Can we just store the 1 and the 3 as numerator and denominator?", which takes us to the next point.
Why not rational?
^^^^^^^^^^^^^^^^^
Rational numbers are stored using two integer numbers, the numerator and the denominator. This implies that the arithmetic operations can't be executed directly (e.g. to add two rational numbers you first need to calculate the common denominator).
Quoting Alex Martelli:
The performance implications of the fact that summing two rationals (which take O(M) and O(N) space respectively) gives a rational which takes O(M+N) memory space is just too troublesome. There are excellent Rational implementations in both pure Python and as extensions (e.g., gmpy), but they'll always be a "niche market" IMHO.
Anyway, if you're interested in this data type, you may take a look at PEP 239 [6]_: Adding a Rational Type to Python.
Decimal floating point
----------------------
So, what we have is a Decimal data type, with bounded precision and floating point. There are several uses for such a data type (I needed to use it as base for Money), and there's a lot of other advantages over ``float``:
- Decimal numbers can be represented exactly. In contrast, numbers like 1.1 do not have an exact representation in binary floating point.
- The exactness carries over into arithmetic. In decimal floating point, "0.1 + 0.1 + 0.1 - 0.3" is exactly equal to zero. In binary floating point, result is 5.5511151231257827e-017. While near to zero, the differences prevent reliable equality testing and differences can accumulate. For this reason, decimal would be preferred in accounting applications which have strict equality invariants.
- This module implements the rules that are taught at school. Up to a given working precision, exact unrounded results are given when possible (for instance, 0.9/10 gives 0.09, not 0.089999999999999997). Where results exceed the working precision, the number gets rounded according the working rounding method.
- The decimal module incorporates notion of significant places so that "1.30 + 1.20" is 2.50. The trailing zero is kept to indicate significance. This is the usual representation for monetary applications. For multiplication, the "schoolbook" approach uses all the figures in the multiplicands. For instance, "1.3 * 1.2" gives 1.56 while "1.30 * 1.20" gives 1.5600.
- Unlike hardware based binary floating point, the decimal module has a user settable precision (defaulting to 28 places) which can be as large as needed for a given problem:
>>> getcontext().prec = 6 # set the precision to 6...
>>> Decimal(1) / Decimal(7)
Decimal("0.142857")
>>> getcontext().prec = 28 # ...to 28...
>>> Decimal(1) / Decimal(7)
Decimal("0.1428571428571428571428571429")
>>> getcontext().prec = 60 # ...and to 60 digits
>>> Decimal(1) / Decimal(7)
Decimal("0.142857142857142857142857142857142857142857142857142857142857")
- The concept of a context for operations is explicit. This allows global rules (such as precision and rounding) to be easily implemented and modified. The context is per thread, so it's possible to change the context of one thread without affecting the others.
- Both binary and decimal floating point are implemented in terms of published standards. While the built-in float type exposes only a modest portion of its capabilities, the decimal module exposes all required parts of the standard [7]_. The module is based on the "General Decimal Arithmetic Specification" [8]_, hereafter called the *Spec*.
Speed issues
^^^^^^^^^^^^
As long Decimal is implemented in software and ``float`` is implemented in hardware, the latter will always be faster.
Right now Decimal is a pure Python module, so it could get a big performance gain if is re-implemented in C/C++. But it'll be never as fast as float unless...
- ...Decimal gets implemented in hardware: There had been approaches to this (processors with decimal arithmetic in silicon), but no mainstream hardware does this nowadays.
- ...there's no float-in-hardware: There're a lot of processors out there without a binary floating point arithmetic unit.
How to use Decimal
==================
Decimal instances can be constructed from integers, strings or tuples. To create a Decimal from a float, first convert it to a string (this serves as an explicit reminder of the details of the conversion, including representation error). Decimal numbers include special values such as NaN which stands for "Not a number", positive and negative Infinity, and -0. Once constructed, Decimal objects are immutable.
Some instantiatons:
>>> Decimal(10)
Decimal("10")
>>> Decimal("3.14")
Decimal("3.14")
>>> Decimal((0, (3, 1, 4), -2))
Decimal("3.14")
>>> Decimal(str(2.0 ** 0.5))
Decimal("1.41421356237")
>>> Decimal(repr(2.0 ** 0.5))
Decimal("1.4142135623730951")
>>> Decimal("2.31e14")
Decimal("2.31E+14")
>>> Decimal("NaN")
Decimal("NaN")
>>> Decimal("-Infinity")
Decimal("-Infinity")
The significance of a new Decimal is determined solely by the number of digits input. Context precision and rounding only come into play during arithmetic operations (the context does not affect how many digits are stored). For example, with a precision of six digits:
>>> getcontext().prec = 6
>>> Decimal('3.0')
Decimal("3.0")
>>> Decimal('3.1415926535')
Decimal("3.1415926535")
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal("5.85987")
>>> +Decimal("3.1415926535")
Decimal("3.14159")
Decimal floating point objects share many properties with the other builtin numeric types such as ``float`` and ``int``. All of the usual math operations and special methods apply (one limitation: exponentiation requires an integer exponent). Likewise, decimal objects can be copied, pickled, printed, used as dictionary keys, used as set elements, compared, sorted, and coerced to another type. In general, Decimals interact well with much of the rest of Python:
>>> data = map(Decimal, '1.34 1.87 3.45 2.35 1.00 0.03 9.25'.split())
>>> min(data), max(data)
(Decimal("0.03"), Decimal("9.25"))
>>> sorted(data)[1:3]
[Decimal("1.00"), Decimal("1.34")]
>>> sum(data)
Decimal("19.29")
>>> a,b = data[:2]
>>> str(a)
'1.34'
>>> float(a)
1.3400000000000001
>>> round(a, 1) # round() first converts to binary floating point
1.3
>>> int(a)
1
>>> a+b
Decimal("3.21")
>>> a-b
Decimal("-0.53")
>>> a*b
Decimal("2.5058")
>>> a/b
Decimal("0.7165775401069518716577540107")
>>> a % b
Decimal("1.34")
>>> a ** 2
Decimal("1.7956")
>>> a + 4
Decimal("5.34")
>>> a + 4.5
Traceback (most recent call last):
...
TypeError: You can interact Decimal only with int, long or Decimal data types.
Decimal numbers can be used with the math and cmath modules, but note that they'll be immediately converted to floating-point numbers before the operation is performed, resulting in a possible loss of precision and accuracy. You'll also get back a regular floating-point number and not a Decimal.
>>> import math, cmath
>>> d = Decimal('123456789012.345')
>>> math.sqrt(d)
351364.18288201344
>>> cmath.sqrt(-d)
351364.18288201344j
Methods
-------
In addition to the standard numeric properties, decimal floating point objects also have a number of specialized methods:
- ``adjusted()``: Return the adjusted exponent after shifting out the coefficient's rightmost digits until only the lead digit remains (used for determining the position of the most significant digit with respect to the decimal point):
>>> Decimal("321e+5").adjusted()
7
>>> Decimal("345e-2").adjusted()
0
- ``as_tuple()``: Returns a tuple representation of the number, showing its internales: ``(sign, digittuple, exponent)``.
>>> Decimal("321e+5").as_tuple()
(0, (3, 2, 1), 5)
>>> Decimal(".336").as_tuple()
(0, (3, 3, 6), -3)
- ``compare(other[, context])``: Compares like ``__cmp__()`` but returns a decimal instance.
>>> Decimal(".336").compare(Decimal(".337"))
Decimal("-1")
>>> Decimal("NaN").compare(Decimal(".337"))
Decimal("NaN")
>>> Decimal(8).compare(Decimal(".337"))
Decimal("1")
>>> Decimal(8).compare(Decimal(8))
Decimal("0")
- ``max(other[, context])``: Like ``max(self, other)`` but returns NaN if both are NaNs. Applies the context rounding rule before returning.
>>> Decimal(8).max(Decimal(3))
Decimal("8")
>>> Decimal("NaN").max(Decimal("NaN"))
Decimal("NaN")
- ``min(other[, context])``: Like ``min(self, other)`` but returns NaN if both are NaNs. Applies the context rounding rule before returning.
>>> Decimal(8).min(Decimal(3))
Decimal("3")
>>> Decimal("NaN").min(Decimal("NaN"))
Decimal("NaN")
- ``normalize([context])``: Normalize the number by stripping the rightmost trailing zeroes (used for producing canonical values for members of an equivalence class).
>>> Decimal("32.100").normalize()
Decimal("32.1")
>>> Decimal("0.321000e+2").normalize()
Decimal("32.1")
- ``quantize(exp [, rounding[, context[, watchexp]]])``: Quantize makes the exponent the same as ``exp``. Searches for a rounding method in ``rounding``, then in ``context``, and then in the current context. If ``watchexp`` is True (default), then an error is returned whenever the resulting exponent is greater than Emax or less than Etiny. This method is useful for monetary applications that often round results to a fixed number of places.
>>> Decimal("3.62736").quantize(Decimal(".0001"))
Decimal("3.6274")
>>> Decimal("7.325").quantize(Decimal(".01"), rounding=ROUND_DOWN)
Decimal("7.32")
- ``remainder_near(other[, context])``: Computes the modulo as either a positive or negative value depending on which one is closest to zero (if both are equally close, the chosen one will have the same sign as ``self``).
>>> Decimal(10).remainder_near(3)
Decimal("1")
>>> Decimal(10).remainder_near(6)
Decimal("-2") # is closer to zero than Decimal("4")
- ``same_quantum(other[, context])``: Test whether ``self`` and ``other`` have the same exponent or whether both are NaN.
>>> Decimal("3.62736").same_quantum(Decimal(".0001"))
False
>>> Decimal("3.6273").same_quantum(Decimal(".0001"))
True
- ``sqrt([context])``: Return the square root to full precision.
>>> Decimal(144).sqrt()
Decimal("12")
>>> Decimal("3.6273").sqrt()
Decimal("1.904547190279096234922408326")
- ``to_eng_string([context])``: Convert to an engineering-type string *if an exponent is needed*. Engineering notation has an exponent which is a multiple of 3, so there are up to 3 digits left of the decimal place.
>>> Decimal("1230").to_eng_string()
'1230'
>>> Decimal("123e1").to_eng_string()
'1.23E+3'
- ``to_integral([rounding[, context]])``: Rounds to the nearest integer without signaling ``Inexact`` or ``Rounded``. If given, applies rounding; otherwise, uses the rounding method in either the supplied context or the current context.
>>> Decimal("2.33").to_integral()
Decimal("2")
>>> getcontext() # check the state of the flags
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, capitals=1,
flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
Structure
---------
As said before, Decimal can be finite numbers or special values. Finite numbers are defined by three parameters:
- ``Sign``: 0 (positive) or 1 (negative).
- ``Coefficient``: a non-negative integer.
- ``Exponent``: a signed integer, the power of ten of the coefficient multiplier.
The numerical value of a finite number is given by::
(-1)**sign * coefficient * 10**exponent
Special values are named as following:
- Infinity: a value which is infinitely large. Could be positive or negative.
- Quiet NaN ("qNaN"): represent undefined results (*Not a Number*). Does not cause an Invalid operation condition.
- Signaling NaN ("sNaN"): also *Not a Number*, but will cause an Invalid operation condition if used in any operation.
The infinities are signed and can be used in arithmetic operations where they get treated as very large, indeterminate numbers (for instance, adding a constant to infinity gives another infinite result). The sign in a NaN has no meaning.
Some operations are indeterminate and return NaN, or if the InvalidOperation signal is trapped, raise an exception. For example, ``0/0`` (not a number) returns NaN. This variety of NaN is quiet and, once created, will flow through other computations always resulting in another NaN. This behavior can be useful for a series of computations that occasionally have missing inputs: it allows the calculation to proceed while flagging specific results as invalid.
A variant is sNaN which signals rather than remaining quiet after every operation. This is a useful return value when an invalid result needs to interrupt a calculation for special handling.
The signed zeros can result from calculations that underflow. They keep the sign that would have resulted if the calculation had been carried out to greater precision. Since their magnitude is zero, both positive and negative zeros are treated as equal and their sign is informational.
How to use Context
==================
Each thread has its own current context which is accessed or changed using the getcontext() and setcontext() functions:
- ``getcontext()``: Return the current context for the active thread. Accesses a different Context object for each thread. Having separate thread contexts means that threads may make changes without interfering with other threads.
- ``setcontext(c)``: Set the current context for the active thread to ``c``. Automatically assigns its target to the current thread. If setcontext() has not been called before getcontext(), the latter will automatically create a new context for use in the current thread.
For example:
>>> from decimal import *
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, capitals=1,
flags=[], traps=[Overflow, DivisionByZero, InvalidOperation])
>>> getcontext().prec = 6
>>> getcontext().rounding = ROUND_DOWN
>>> getcontext()
Context(prec=6, rounding=ROUND_DOWN, Emin=-999999999, Emax=999999999, capitals=1,
flags=[], traps=[Overflow, DivisionByZero, InvalidOperation])
Working with Context
--------------------
The previous approach meets the needs of most applications, but for more advanced work, it may be useful to create alternate contexts using the ``Context()`` constructor. To activate other context, use the ``setcontext()`` function. In accordance with the standard, the Decimal module provides two ready to use standard contexts, BasicContext and ExtendedContext:
>>> othercontext = Context(prec=60, rounding=ROUND_HALF_DOWN)
>>> setcontext(othercontext)
>>> Decimal(1) / Decimal(7)
Decimal("0.142857142857142857142857142857142857142857142857142857142857")
>>> ExtendedContext
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, capitals=1,
flags=[], traps=[])
>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(7)
Decimal("0.142857143")
>>> Decimal(42) / Decimal(0)
Decimal("Infinity")
>>> BasicContext # useful for debugging, many of the traps are enabled
Context(prec=9, rounding=ROUND_HALF_UP, Emin=-999999999, Emax=999999999, capitals=1,
flags=[], traps=[Underflow, Clamped, InvalidOperation, Overflow, DivisionBy Zero])
>>> setcontext(BasicContext)
>>> Decimal(42) / Decimal(0)
Traceback (most recent call last):
...
DivisionByZero: x / 0
There is also the ``DefaultContext``, which is used by the Context constructor as a prototype for new contexts. Changing a field (such a precision) has the effect of changing the default for new contexts created by the Context constructor. This context is most useful in multi-threaded environments: changing one of the fields before threads are started has the effect of setting system-wide defaults (changing the fields after threads have started is not recommended as it would require thread synchronization to prevent race conditions).
In single threaded environments it's not necessary to use this context at all (just create contexts explicitly as described below). The default values for this context are:
>>> DefaultContext
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, capitals=1,
flags=[], traps=[Overflow, InvalidOperation, DivisionByZero])
Signals are groups of exceptional conditions arising during the course of computation. Depending on the needs of the application, signals may be ignored, considered as informational, or treated as exceptions.
For each signal there is a flag and a trap enabler. When a signal is encountered, its flag is incremented from zero and, then, if the trap enabler is set to one, an exception is raised. Flags are sticky, so the user needs to reset them before monitoring a calculation, by using the clear_flags() method.
>>> setcontext(ExtendedContext)
>>> getcontext() # no flags
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999,
capitals=1, flags=[], traps=[])
>>> Decimal(355) / Decimal(113)
Decimal("3.14159292")
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999,
capitals=1, flags=[Inexact, Rounded], traps=[])
>>> getcontext().clear_flags()
>>> getcontext() # again no flags
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999,
capitals=1, flags=[], traps=[])
The flags entry shows that the result was rounded (digits beyond the context precision were thrown away) and that it's inexact (some of the discarded digits were non-zero). We'll get into all the available flags and their meaning in `Signals`_.
To change the behaviour of raising or not an exception for each signal, modify the individual traps using the context's trap dictionary:
>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(0)
Decimal("Infinity")
>>> getcontext().traps[DivisionByZero] = True
>>> Decimal(1) / Decimal(0)
Traceback (most recent call last):
...
DivisionByZero: x / 0
Most programs adjust the current context only once, at the beginning of the program. For example, a typical use would be to set the context's precision to 20 digits at the start of a program, and never explicitly use context again. It's up to the application to work with one or several contexts, but definitely the idea is not to get a context per Decimal number.
Structure
---------
All the context attributes can be set also when creating a new context with::
Context(prec=None, rounding=None, traps=None, flags=None, Emin=None, Emax=None, capitals=1)
If a field is not specified or is None, the default values are copied from the DefaultContext. If the flags field is not specified or is None, all flags are cleared.
- The prec field is a positive integer that sets the precision.
- The rounding option (see `Rounding Algorithms`_).
- The traps and flags fields list any signals to be set. Generally, new contexts should only set traps and leave the flags clear.
- The Emin and Emax fields are integers specifying the outer limits allowable for exponents.
- The capitals field is either 0 or 1 (the default). If set to 1, exponents are printed with a capital E; otherwise, a lowercase e is used: ``Decimal('6.02e+23')``.
Methods
-------
The Context class defines several general purpose methods as well as a large number of methods for doing arithmetic directly in a given context. These methods are similar to those for the Decimal class; I won't detail them here but here're some examples:
>>> getcontext().prec = 5
>>> othercontext = Context(prec=9)
>>> d = Decimal("1234.5678")
>>> d + 3
Decimal("1237.6")
>>> othercontext.add(d, 3)
Decimal("1237.5678")
>>> d.sqrt()
Decimal("35.136")
>>> othercontext.sqrt(d)
Decimal("35.1364170")
The following are the Context general purpose methods:
- ``clear_flags()``: Sets all of the flags to 0.
- ``copy()``: Returns a duplicate of the context.
- ``create_decimal(num)``: Creates a new Decimal instance from ``num`` but using ``self`` as context. Unlike the Decimal constructor, the context precision, rounding method, flags, and traps are applied to the conversion.
This is useful because constants are often given to a greater precision than is needed by the application. Another benefit is that rounding immediately eliminates unintended effects from digits beyond the current precision.
- ``Etiny()``: Returns a value equal to "Emin - prec + 1" which is the minimum exponent value for subnormal results. When underflow occurs, the exponent is set to Etiny.
- ``Etop()``: Returns a value equal to "Emax - prec + 1".
Rounding Algorithms
-------------------
The following are the available rounding algorithms in Decimal:
- ``round-down``: The discarded digits are ignored; the result is unchanged (round toward 0, truncate).
- ``round-up``: If all of the discarded digits are zero the result is unchanged, otherwise the result is incremented by 1 (round away from 0).
- ``round-half-up``: If the discarded digits represent greater than or equal to half (0.5) then the result should be incremented by 1; otherwise the discarded digits are ignored.
- ``round-half-down``: If the discarded digits represent greater than half (0.5) then the result is incremented by 1; otherwise the discarded digits are ignored.
- ``round-half-even``: If the discarded digits represent greater than half (0.5) then the result coefficient is incremented by 1; if they represent less than half, then the result is not adjusted; otherwise the result is unaltered if its rightmost digit is even, or incremented by 1 if its rightmost digit is odd (to make an even digit).
- ``round-ceiling``: If all of the discarded digits are zero or if the sign is negative the result is unchanged; otherwise, the result is incremented by 1 (round toward positive infinity).
- ``round-floor``: If all of the discarded digits are zero or if the sign is positive the result is unchanged; otherwise, the absolute value of the result is incremented by 1 (round toward negative infinty).
Signals
-------
The following are the available Signals:
- ``Clamped``: An exponent was altered to fit representation constraints. Typically, clamping occurs when an exponent falls outside the context's ``Emin`` and ``Emax`` limits (if possible, the exponent is reduced to fit by adding zeroes to the coefficient).
- ``DivisionByZero``: Signals the division of a non-infinite number by zero. It can occur with division, modulo division, or when raising a number to a negative power.
- ``Inexact``: Indicates that rounding occurred and the result is not exact. Signals when non-zero digits were discarded during rounding.
- ``InvalidOperation``: An invalid operation was performed. Indicates that an operation was requested that does not make sense (e.g.: ``Infinity - Infinity``, ``0 * Infinity``, etc.).
- ``Overflow``: Numerical overflow. Indicates the exponent is larger than Emax after rounding. ``Inexact`` and ``Rounded`` are also signaled.
- ``Rounded``: Rounding occurred though possibly no information was lost. Signaled whenever rounding discards digits; even if those digits are zero (such as rounding 5.00 to 5.0). This signal is used to detect loss of significant digits.
- ``Subnormal``: Exponent was lower than Emin prior to rounding. Occurs when an operation result is subnormal (the exponent is too small).
- ``Underflow``: Numerical underflow with result rounded to zero. Occurs when a subnormal result is pushed to zero by rounding. ``Inexact`` and ``Subnormal`` are also signaled.
When to use Decimal
===================
Financial
---------
Currency calculations are often defined in terms of a given precision (for instance, regulations dictate that Euro exchange rates must be quoted to 6 digits). All the digits must be present, even if some trailing fractional digits are zero. For example, 1 Euro = 340.750 Greek drachmas.
When dealing with money you'll always want to have exact quantities. For example if you have to add a 5% to a 0.70 bill:
>>> Decimal("1.05")*Decimal("0.70")
Decimal("0.7350")
>>> 1.05*.7
0.73499999999999999
Note that in the float calculus you get something that is less than 0.735, so how sure are you that it'll round up to 0.74?
Easier life
-----------
Perils of binary floating point comparison:
>>> f = .1
>>> f+f+f+f+f+f+f+f
0.79999999999999993
>>> f*8
0.80000000000000004
>>> f+f+f+f+f+f+f+f == f*8
False
but with decimal:
>>> d = Decimal(".1")
>>> d+d+d+d+d+d+d+d
Decimal("0.8")
>>> d*8
Decimal("0.8")
>>> d+d+d+d+d+d+d+d == d*8
True
Big precision
-------------
With the possibility of setting the precision to any value, it's possible to avoid the issues of working near that limit.
Knuth provides two instructive examples where rounded floating point arithmetic with insufficient precision causes the breakdown of the associative and distributive properties of addition (examples from Seminumerical Algorithms, Section 4.2.2):
>>> getcontext().prec = 8
>>> u, v, w = map(Decimal, (11111113, -11111111, '7.51111111'))
>>> (u + v) + w
Decimal("9.5111111")
>>> u + (v + w)
Decimal("10")
>>>
>>> u, v, w = map(Decimal, (20000, -6, '6.0000003'))
>>> (u*v) + (u*w)
Decimal("0.01")
>>> u * (v+w)
Decimal("0.0060000")
The decimal module makes it possible to restore the identities by expanding the precision sufficiently to avoid loss of significance:
>>> getcontext().prec = 20
>>> u, v, w = map(Decimal, (11111113, -11111111, '7.51111111'))
>>> (u + v) + w
Decimal("9.51111111")
>>> u + (v + w)
Decimal("9.51111111")
>>>
>>> u, v, w = map(Decimal, (20000, -6, '6.0000003'))
>>> (u*v) + (u*w)
Decimal("0.0060000")
>>> u * (v+w)
Decimal("0.0060000")
When *not* to use Decimal
=========================
Speed is more important than accuracy
-------------------------------------
This happens a lot in standard scientific applications, when generally some error is allowed and other errors are bigger than the originated by binary floating point.
>>> dpi = pi() # using recipe from docs
>>> dpi
Decimal("3.141592653589793238462643383")
>>> import math
>>> mpi = math.pi
>>> mpi
3.1415926535897931
>>> radius = 6378 # earth radius in kilometers
>>> m_perim = 2 * mpi * radius
>>> m_perim
40074.155889191403
>>> d_perim = 2 * dpi * radius
>>> d_perim
Decimal("40074.15588919140254982947899")
>>> diff = d_perim - Decimal(repr(m_perim))
>>> diff * 10**12
Decimal("-0.45017052101000000000000") # difference in nanometers
Here, the error introduced because of working with ``float`` is a lot smaller than other errors (measuring the Earth radius). Not having the binary floating point error is not enough reason to switch to Decimal and have a slower math.
Directly as a Money data type
-----------------------------
Decimal can't be used as Money without further effort. In the following example it is showed that, if you'll work with two digits after the decimal point (up to the cents), it's not the same to round after or between the calculations.
>>> loan = Decimal("10.85")
>>> percent_rate = Decimal(".3")
>>> interest = loan * percent_rate / 100
>>> interest
Decimal("0.03255")
>>> interest.quantize(Decimal(".01"))
Decimal("0.03")
>>> interest.quantize(Decimal(".01")) * 10
Decimal("0.30")
>>> interest * 10
Decimal("0.32550")
>>> (interest * 10).quantize(Decimal(".01"))
Decimal("0.33")
It's my purpose to build some day a Money data type. If you're also interested in it, stay tuned to python lists (or just mail me).
Acknowledgments
===============
The module is based on code and test functions written by Eric Price, Aahz and Tim Peters. Thanks to them because of the kick-off.
Raymond Hettinger made a **lot** of work with the documentation, clean ups, optimization and other activities that made Decimal a module at the level of the standard library. I couldn't have done it without his effort.
I would like to specially thank to Tim Peters, Raymond Hettinger, Alex Martelli and Aahz for their fantastic all-level help.
Thanks to Chaghi, Diego Quevedo y Mario Zorz for reviewing of this paper.
References
==========
.. [1] Look for decimal package in SiGeFi project: http://sourceforge.net/projects/sigefi
.. [2] Decimal download instructions: http://www.taniquetil.com.ar/facundo/bdvfiles/get_decimal.html
.. [3] Decimal documentation (FIXME: sacar que es todavia devel): http://www.python.org/dev/doc/devel/lib/module-decimal.html
.. [4] In all the examples of this paper the module is always implicitly imported as ``from decimal import *``.
.. [5] Tim Peter's FixedPoint at SourceForge: http://fixedpoint.sourceforge.net/
.. [6] PEP 239, "Adding a Rational Type to Python": http://www.python.org/peps/pep-0239.html
.. [7] Related documents and links at http://www2.hursley.ibm.com/decimal/
.. [8] General Decimal Arithmetic specification (Cowlishaw): http://www2.hursley.ibm.com/decimal/decarith.html
..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
End: