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. _______________________________________________ Python-Dev mailing list Python-Dev@python.org http://mail.python.org/mailman/listinfo/python-dev Unsubscribe: http://mail.python.org/mailman/options/python-dev/archive%40mail-archive.com