Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Modern Python Cookbook

You're reading from   Modern Python Cookbook The latest in modern Python recipes for the busy modern programmer

Arrow left icon
Product type Paperback
Published in Nov 2016
Publisher Packt
ISBN-13 9781786469250
Length 692 pages
Edition 1st Edition
Languages
Arrow right icon
Toc

Table of Contents (12) Chapters Close

Preface 1. Numbers, Strings, and Tuples 2. Statements and Syntax FREE CHAPTER 3. Function Definitions 4. Built-in Data Structures – list, set, dict 5. User Inputs and Outputs 6. Basics of Classes and Objects 7. More Advanced Class Design 8. Input/Output, Physical Format, and Logical Layout 9. Testing 10. Web Services 11. Application Integration

Choosing between float, decimal, and fraction

Python offers us several ways to work with rational numbers and approximations of irrational numbers. We have three basic choices:

  • Float
  • Decimal
  • Fraction

With so many choices, when do we use each of these?

Getting ready

It's important to be sure about our core mathematical expectations. If we're not sure what kind of data we have, or what kinds of results we want to get, we really shouldn't be coding. We need to take a step back and review things with pencil and paper.

There are three general cases for math that involve numbers beyond integers, which are:

  1. Currency: Dollars, cents, or euros. Currency generally has a fixed number of decimal places. There are rounding rules used to determine what 7.25% of $2.95 is.
  2. Rational Numbers or Fractions: When we're working with American units for feet and inches, or cooking measurements in cups and fluid ounces, we often need to work in fractions. When we scale a recipe that serves eight, for example, down to five people, we're doing fractional math using a scaling factor of 5/8 . How do we apply this to 2/3 cup of rice and still get a measurement that fits an American kitchen gadget?
  3. Irrational Numbers: This includes all other kinds of calculations. It's important to note that digital computers can only approximate these numbers, and we'll occasionally see odd little artifacts of this approximation. The float approximations are very fast, but sometimes suffer from truncation issues.

When we have one of the first two cases, we should avoid floating-point numbers.

How to do it...

We'll look at each of the three cases separately. First, we'll look at computing with currency. Then we'll look at rational numbers, and finally irrational or floating-point numbers. Finally, we'll look at making explicit conversions among these various types.

Doing currency calculations

When working with currency, we should always use the decimal module. If we try to use Python's built-in float values, we'll have problems with rounding and truncation of numbers.

  1. To work with currency, we'll do this. Import the Decimal class from the decimal module:
      >>> from decimal import Decimal
  1. Create Decimal objects from strings or integers:
      >>> from decimal import Decimal
      >>> tax_rate = Decimal('7.25')/Decimal(100)
      >>> purchase_amount = Decimal('2.95')
      >>> tax_rate * purchase_amount
      Decimal('0.213875')

We created the tax_rate from two Decimal objects. One was based on a string, the other based on an integer. We could have used Decimal('0.0725') instead of doing the division explicitly.

The result is a hair over $0.21. It's computed out correctly to the full number of decimal places.

  1. If you try to create decimal objects from floating-point values, you'll see unpleasant artifacts of float approximations. Avoid mixing Decimal and float. To round to the nearest penny, create a penny object:
      >>> penny=Decimal('0.01')
  1. Quantize your data using this penny object:
      >>> total_amount = purchase_amount + tax_rate*purchase_amount
      >>> total_amount.quantize(penny)
      Decimal('3.16')

This shows how we can use the default rounding rule of ROUND_HALF_EVEN.

Every financial wizard has a different style of rounding. The Decimal module offers every variation. We might, for example, do something like this:

>>> import decimal
>>> total_amount.quantize(penny, decimal.ROUND_UP)
Decimal('3.17')

This shows the consequences of using a different rounding rule.

Fraction calculations

When we're doing calculations that have exact fraction values, we can use the fractions module. This provides us handy rational numbers that we can use. To work with fractions, we’ll do this:

  1. Import the Fraction class from the fractions module:
      >>> from fractions import Fraction
  1. Create Fraction objects from strings, integers, or pairs of integers. If you create fraction objects from floating-point values, you may see unpleasant artifacts of float approximations. When the denominator is a power of 2, things can work out exactly:
      >>> from fractions import Fraction
      >>> sugar_cups = Fraction('2.5')
      >>> scale_factor = Fraction(5/8)
      >>> sugar_cups * scale_factor
      Fraction(25, 16)

We created one fraction from a string, 2.5. We created the second fraction from a floating-point calculation, 5/8. Because the denominator is a power of 2, this works out exactly.

The result, 25/16, is a complex-looking fraction. What's a nearby fraction that might be simpler?

    >>> Fraction(24,16)
    Fraction(3, 2)

We can see that we'll use almost a cup and a half to scale the recipe for five people instead of eight.

Floating-point approximations

Python's built-in float type is capable of representing a wide variety of values. The trade-off here is that float often involves an approximation. In some cases—specifically when doing division that involves powers of 2—it can be as exact as a fraction. In all other cases, there may be small discrepancies that reveal the differences between the implementation of float and the mathematical ideal of an irrational number.

  1. To work with float, we often need to round values to make them look sensible. Recognize that all calculations are an approximation:
      >>> (19/155)*(155/19)
      0.9999999999999999
  1. Mathematically, the value should be 1. Because of the approximations used for float, the answer isn't exact. It's not wrong by much, but it's wrong. When we round appropriately, the value is more useful:
      >>> answer= (19/155)*(155/19)
      >>> round(answer, 3)
      1.0
  1. Know the error term. In this case, we know what the exact answer is supposed to be, so we can compare our calculation with the known correct answer. This gives us the general error value that can creep into floating-point numbers:
      >>> 1-answer
      1.1102230246251565e-16

For most floating-point errors, this is the typical value—about 10-16. Python has clever rules that hide this error some of the time by doing some automatic rounding. For this calculation, however, the error wasn't hidden.

This is a very important consequence.

Don't compare floating-point values for exact equality.

When we see code that uses an exact == test between floating-point numbers, there are going to be problems when the approximations differ by a single bit.

Converting numbers from one type to another

We can use the float() function to create a float value from another value. It looks like this:

>>> float(total_amount)
3.163875
>>> float(sugar_cups * scale_factor)
1.5625

In the first example, we converted a Decimal value to float. In the second example, we converted a Fraction value to float.

As we just saw, we're never happy trying to convert float to Decimal or Fraction:

>>> Fraction(19/155)
Fraction(8832866365939553, 72057594037927936)
>>> Decimal(19/155)
Decimal('0.12258064516129031640279123394066118635237216949462890625')

In the first example, we did a calculation among integers to create a float value that has a known truncation problem. When we created a Fraction from that truncated float value, we got some terrible looking numbers that exposed the details of the truncation.

Similarly, the second example tried to create a Decimal value from a float.

How it works...

For these numeric types, Python offers us a variety of operators: +, -, *, /, //, %, and **. These are for addition, subtraction, multiplication, true division, truncated division, modulus, and raising to a power. We'll look at the two division operators in the Choosing between true division and floor division recipe.

Python is adept at converting numbers between the various types. We can mix int and float values; the integers will be promoted to floating-point to provide the most accurate answer possible. Similarly, we can mix int and Fraction and the results will be Fractions. We can also mix int and Decimal. We cannot casually mix Decimal with float or Fraction; we need to provide explicit conversions.

It's important to note that float values are really approximations. The Python syntax allows us to write numbers as decimal values; that's not how they're processed internally.

We can write a value like this in Python, using ordinary base-10 values:

>>> 8.066e+67
8.066e+67

The actual value used internally will involve a binary approximation of the decimal value we wrote.

The internal value for this example, 8.066e+67, is this:

>>> 6737037547376141/2**53*2**226
8.066e+67

The numerator is a big number, 6737037547376141. The denominator is always 253. Since the denominator is fixed, the resulting fraction can only have 53 meaningful bits of data. Since more bits aren't available, values might get truncated. This leads to tiny discrepancies between our idealized abstraction and actual numbers. The exponent (2226) is required to scale the fraction up to the proper range.

Mathematically, 6737037547376141 * 2226/253.

We can use math.frexp() to see these internal details of a number:

>>> import math
>>> math.frexp(8.066E+67)
(0.7479614202861186, 226)

The two parts are called the mantissa and the exponent. If we multiply the mantissa by 253, we always get a whole number, which is the numerator of the binary fraction.

The error we noticed earlier matches this quite nicely: 10-16 ≈ 2-53 .

Unlike the built-in float, a Fraction is an exact ratio of two integer values. As we saw in the Working with large and small integers recipe, integers in Python can be very large. We can create ratios which involve integers with a large number of digits. We're not limited by a fixed denominator.

A Decimal value, similarly, is based on a very large integer value, and a scaling factor to determine where the decimal place goes. These numbers can be huge and won't suffer from peculiar representation issues.

Why use floating-point? Two reasons: Not all computable numbers can be represented as fractions. That's why mathematicians introduced (or perhaps discovered) irrational numbers. The built-in float type is as close as we can get to the mathematical abstraction of irrational numbers. A value like √2, for example, can't be represented as a fraction. Also, float values are very fast.

There's more...

The Python math module contains a number of specialized functions for working with floating-point values. This module includes common functions such as square root, logarithms, and various trigonometry functions. It has some other functions such as gamma, factorial, and the Gaussian error function.

The math module includes several functions that can help us do more accurate floating-point calculations. For example, the math.fsum() function will compute a floating-point sum more carefully than the built-in sum() function. It's less susceptible to approximation issues.

We can also make use of the math.isclose() function to compare two floating-point values to see if they're nearly equal:

>>> (19/155)*(155/19) == 1.0
False
>>> math.isclose((19/155)*(155/19), 1)
True

This function provides us with a way to compare floating-point numbers meaningfully.

Python also offers complex data. This involves a real and an imaginary part. In Python, we write 3.14+2.78j to represent the complex number 3.14 + 2.78 √-1. Python will comfortably convert between float and complex. We have the usual group of operators available for complex numbers.

To support complex numbers, there's a cmath package. The cmath.sqrt() function, for example, will return a complex value rather than raise an exception when extracting the square root of a negative number. Here's an example:

>>> math.sqrt(-2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> cmath.sqrt(-2)
1.4142135623730951j

This is essential when working with complex numbers.

See also

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at R$50/month. Cancel anytime