4

Mathematical Modules in Python: Decimal and Fractions

 2 years ago
source link: https://code.tutsplus.com/tutorials/mathematical-modules-in-python-decimal-and-fractions--cms-27691
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Mathematical Modules in Python
by Monty ShokeenJan 16, 2017(Updated Apr 25, 2022)
Read Time:8 minsLanguages:

Even the most basic mathematical operations can sometimes give an erroneous result. This happens due to limitations in storing the exact value of some numbers. You can overcome these limitations by using the decimal module in Python. Similarly, neither the math nor the cmath module that we learned about in our last tutorial can help us in doing fraction-based arithmetic. However, the fractions module in Python does exactly that.

In this tutorial, you will learn about both these modules and the different functions they make available.

Using the Decimal Module

Task Typical Functions

Creating a Decimal Number

Decimal() constructor   Using Context to Set Rounding and Precision

getcontext().prec, getcontext().rounding

  Math Operations on Decimals

sqrt(), exp(), log()

  Working with Context in the Decimal Module

setcontext(), localcontext()

 

Using the Fractions Module

Task Typical Code

Creating Fractions

Fraction() constructor   Arithmetic With Fractions

+, -, *, / operators   Numerator and Denominator Functions

limit_denominator() function, numerator, denominator properties   Fractions and the Math Module

math.sqrt(), math.floor()

 

Why Do We Need a Decimal Module?

You are probably wondering why we need a module to do basic arithmetic with decimal numbers when we can already do the same using floats.

Before I answer this question, I want you to take a guess about the output value if you type 0.1 + 0.2 in the Python console. If you guessed that the output should be 0.3, you will be surprised when you check out the actual result, which is 0.30000000000000004. You can try another calculation, like 0.05 + 0.1, and you will get 0.15000000000000002.

To understand what's going on here, try to represent 1/3 in decimal form, and you will notice that the number is actually non-terminating in base 10. Similarly, some numbers like 0.1 or 1/10 are non-terminating in base 2. Since these numbers still need to be represented somehow, a few approximations are made while storing them, which results in those errors.

The number 0.30000000000000004 is actually very close to 0.3, so we can get away with this approximation most of the time. Unfortunately, this approximation is not going to cut it when you are simulating a satellite launch or dealing with money. Another problem with these approximations is that the errors keep piling up.

To get precise results like the ones we are used to dealing with when doing calculations by hand, we need something that supports fast, correctly rounded, decimal floating-point arithmetic, and the decimal module does exactly that.

Using the Decimal Module

Before using the module, you need to import it. After that, you can create decimals from integers, strings, floats, or tuples.

Creating a Decimal Number

When the decimal is constructed from an integer or a float, there is an exact conversion of the value of that number. Take a look at the examples below to see what I mean:

from decimal import Decimal
Decimal(121)
# returns Decimal('121')
Decimal(0.05)
# returns Decimal('0.05000000000000000277555756')
Decimal('0.05')
# returns Decimal('0.05')
Decimal((0, (8, 3, 2, 4), -3))
# returns Decimal('8.324')
Decimal((1, (8, 3, 2, 4), -1))
# returns Decimal('-832.4')

As you can see, the value of Decimal(0.05) is slightly different from Decimal('0.05'). This means that when you add 0.05 and 0.1, you should use decimal.Decimal('0.05') and decimal.Decimal('0.1') to construct the decimals.

from decimal import Decimal
Decimal('0.05') + Decimal('0.1')
# returns Decimal('0.15')
Decimal(0.05) + Decimal(0.1)
# returns Decimal('0.1500000000000000083266726847')

Using Context to Set Rounding and Precision

Now that you can perform various operations on decimals, you might want to control the precision or rounding for those operations. This can be done by using the getcontext() function. This function allows you to get as well as set the value of the precision and rounding options, among other things.

Please keep in mind that both rounding and precision come into play only during arithmetic operations and not while you are creating the decimals themselves.

import decimal
from decimal import Decimal, getcontext
Decimal(1) / Decimal(13)
# returns Decimal('0.07692307692307692307692307692')
getcontext().prec = 10
Decimal(0.03)
# returns Decimal('0.02999999999999999888977697537')
Decimal(1) / Decimal(7)
# returns Decimal('0.1428571429')
getcontext().rounding = decimal.ROUND_DOWN
Decimal(1) / Decimal(7)
# returns Decimal('0.1428571428')
Advertisement

Math Operations on Decimals

You can also use some of the mathematical functions like sqrt(), exp(), and log() with decimals. Here are a few examples:

import decimal
from decimal import Decimal, getcontext
Decimal(2).sqrt()
# returns Decimal('1.414213562373095048801688724')
getcontext().prec = 4
Decimal('2').sqrt()
# returns Decimal('1.414')
Decimal('2000').log10()
# returns Decimal('3.301')

Working With Context in the Decimal Module

We briefly touched upon the concept of context in the previous section when we used the getcontext() function. The context objects in Python's decimal module are used to determine a lot of things like the precision, rounding rules, and exception raising behavior while performing arithmetic calculations.

You can get and set the current context for calculations using the getcontext() and setcontext() functions. Using the localcontext() function alongside the with statement allows you to temporarily change the context for calculations.

There are three built-in contexts in the module that you can use for your calculations. The BasicContext sets precision to nine and the rounding algorithm to ROUND_HALF_UP. The ExtendedContext also keeps precision at nine but sets the rounding algorithm to ROUND_HALF_EVEN. Finally, the DefaultContext sets the precision to 28 but keeps ROUND_HALF_EVEN as its rounding algorithm. Another difference among these contexts is the exception-raising behavior. No exceptions are raised with ExtendedContext. Three exceptions are present in the DefaultContext related to numerical overflow, invalid operation, and division by zero. Almost all exceptions are enabled for BasicContext.

This makes BasicContext ideal for debugging and ExtendedContext ideal for situations where you don't want to halt program execution. As you might have guessed, the DefaultContext is used as the default context for the calculations.

Here is an example of using different contexts to get different results for a simple division:

import decimal
from decimal import ROUND_DOWN, ROUND_UP, Decimal as D
dec_a = D('0.153')
dec_b = D('0.231')
zero = D('0')
print("No Context (Using Default):  ", dec_a/dec_b)
# No Context (Using Default):   0.6623376623376623376623376623
decimal.setcontext(decimal.BasicContext)
print("Basic Context: ", dec_a/dec_b)
# Basic Context:  0.662337662
decimal.setcontext(decimal.ExtendedContext)
print("Extended Context: ", dec_a/dec_b)
# Extended Context:  0.662337662
print("Extended Context: ", dec_b/zero)
# Extended Context:  Infinity
decimal.setcontext(decimal.DefaultContext)
print("Default Context: ", dec_a/dec_b)
# Default Context:  0.6623376623376623376623376623
with decimal.localcontext() as l_ctx:
l_ctx.prec = 5
l_ctx.rounding = ROUND_UP
print("Local Context: ", dec_a/dec_b)
# Local Context:  0.66234

Besides noticing the difference in precision and rounding algorithm for different contexts, you have probably also observed that a division by 0 under ExtendedContext did not raise an exception but output the result as Infinity.

A lot of functions in the decimal module also accept a context object as an argument for performing their calculations. This way, you can avoid constantly setting the context or precision values for computation.

import decimal
from decimal import Decimal as D
print(D('22').sqrt(decimal.BasicContext))
# 4.69041576
print(D('22').sqrt(decimal.ExtendedContext))
# 4.69041576
print(D('22').sqrt(decimal.DefaultContext))
# 4.690415759823429554565630114
with decimal.localcontext() as l_ctx:
l_ctx.prec = 5
print(D('22').sqrt(l_ctx))
# 4.6904

Using the Fractions Module

Sometimes, you might face situations where you need to perform various operations on fractions or the final result needs to be a fraction. The fractions module can be of great help in these cases.

Advertisement

Creating Fractions

The fractions module allows you to create a Fraction instance from numbers, floats, decimals, and even strings. Just like the decimal module, there are a few issues with this module as well when it comes to creating fractions from floats. Here are a few examples:

from fractions import Fraction
from decimal import Decimal
Fraction(11, 35)
# returns Fraction(11, 35)
Fraction(10, 18)
# returns Fraction(5, 9)
Fraction('8/25')
# returns Fraction(8, 25)
Fraction(1.13)
# returns Fraction(1272266894732165, 1125899906842624)
Fraction('1.13')
# returns Fraction(113, 100)
Fraction(Decimal('1.13'))
# returns Fraction(113, 100)

Arithmetic With Fractions

You can also perform simple mathematical operations like addition and subtraction on fractions, just like regular numbers.

from fractions import Fraction
Fraction(113, 100) + Fraction(25, 18)
# returns Fraction(2267, 900)
Fraction(18, 5) / Fraction(18, 10)
# returns Fraction(2, 1)
Fraction(18, 5) * Fraction(16, 19)
# returns Fraction(288, 95)
Fraction(18, 5) * Fraction(15, 36)
# returns Fraction(3, 2)
Fraction(12, 5) ** Fraction(12, 10)
# returns 2.8592589556010197

Numerator and Denominator Functions

The module also has a few important methods like limit_denominator(max_denominator) which will find and return a fraction closest in value to the given fraction whose denominator is at most max_denominator. You can also return the numerator of a given fraction in the lowest term by using the numerator property and the denominator by using the denominator property.

from fractions import Fraction
Fraction('3.14159265358979323846')
# returns Fraction(157079632679489661923, 50000000000000000000)
Fraction('3.14159265358979323846').limit_denominator(10000)
# returns Fraction(355, 113)
Fraction('3.14159265358979323846').limit_denominator(100)
# returns Fraction(311, 99)
Fraction('3.14159265358979323846').limit_denominator(10)
# returns Fraction(22, 7)
Fraction(125, 50).numerator
# returns 5
Fraction(125, 50).denominator
# returns 2

Fractions and the Math Module

You can also use this module with various functions in the math module to perform fraction-based calculations.

import math
from fractions import Fraction
math.sqrt(Fraction(25, 4))
# returns 2.5
math.sqrt(Fraction(28,3))
# returns 3.0550504633038935
math.floor(Fraction(3558, 1213))
# returns 2
Fraction(math.sin(math.pi/3))
# returns Fraction(3900231685776981, 4503599627370496)
Fraction(math.sin(math.pi/3)).limit_denominator(10)
# returns Fraction(6, 7)

Final Thoughts

These two modules should be sufficient to help you perform common operations on both decimals and fractions. As shown in the last section, you can use these modules along with the math module to calculate the value of all kinds of mathematical functions in the format you desire.

In the next tutorial of the series, you will learn about the random module in Python.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK