2023-01-23
We can use Python as a scientific calculator to perform simple arithmetic.
Although this kind of interactive use employs only a small fraction of Python’s capabilities, it is the natural place to start before tackling more advanced topics on vectors, functions and plotting in later modules.
In this lesson, you will learn how to evaluate arithmetic expressions in Python and understand the results produced, particularly when scientific notation, complex numbers, and infinite or undefined answers are involved.
After completing the lesson you should know
inf (infinity) and nan (not-a-number)Although Python’s arithmetic is reasonably intuitive, certain intrinsic limitations of digital computers lead to some subtleties that can be confusing when you first meet them.
The Python symbol for each of the five main arithmetic operators is shown in the following table. The first four are obvious.
| Operator | Symbol |
|---|---|
| Addition | + |
| Subtraction | - |
| Multiplication | * |
| Division | / |
| Exponentiation | ** |
In an arithmetic expression, the order of precedence is as follows.
Operations with the same precedence are evaluated from left to right.
Try out the following examples. In each case, first apply the precedence rules to calculate the answer yourself, and then type the expression in the IPython pane of Spyder to confirm Python’s answer.
1 + 2*3
(1+2)*3
4/2 + 1
1 + 4/2
4/(2+1)
8**2 / 3
8**(2/3)
12/2/3
12/(2*3)
4**3**2
4**(3**2)
(4**3)**2
2*-3
2**-3
In the last two expressions, the minus sign is interpreted as a unary rather than a binary operator. The unary minus has a higher precedence than * and ** so the results are 2*(-3) and 2**(-3).
In practice, if you are unsure about how Python will evaluate an expression, it is best to use explicit parentheses. Doing so will make your code easier to read.
Exercise Type a command to evaluate the fraction with numerator 17.1+20.3 and denominator 36.5+41.8. Hint: the answer is not 17.1 + 20.3/36.5 + 41.8.
The type function tells you the type of any data object in Python. For example, the commands
type(3)
type(3.0)
show that 3 is an int whereas 3.0 is a float. Not surprisingly, int is short for ‘integer’ (i.e., a whole number), but what about float?
For any real number, we can ‘float’ the decimal point \(n\) places to the left or right by incorporating a factor of \(10^n\) or \(10^{-n}\). For example, \[
2\,347.10293=2.347\,102\,93\times10^3=234\,710\,293\times10^{-5}.
\] Any positive real number has a unique representation as a normalised floating-point number, that is, as a product \(m\times10^e\) where \(1\le m<10\) and \(e\) is an integer. The number \(m\) is called the mantissa (or significand) and \(e\) is called the exponent.
For human convenience, Python displays floating-point numbers in base 10, but the computer hardware works in binary, so the real floating-point representation is really \(m\times2^e\) with \(1\le m<2\). Moreover, for reasons of efficiency, all modern computer CPUs are designed to work with a standard 64-bit floating-point format. This fixed size imposes fundamental limits on the magnitude of a floating-point number and on the number of significant figures that can be retained.
The NumPy function finfo provides information about the machine limits of floating-point data types. By typing the command
print(finfo(float))
you can see that the binary exponent ranges from \(-1022\) to \(+1024\), and that the largest floating-point number is about \(1.8\times10^{308}\). The smallest positive normalised floating-point number is about \(2.2\times10^{-308}\) but if we allow a mantissa smaller than \(1\) then we can get as low as \(4.9\times10^{-324}\).
The most important parameter of a floating-point number system is the machine epsilon, defined as the smallest floating-point number \(\epsilon\) such that the computed values of \(1+\epsilon\) is strictly greater than \(1\). Thus, if \(x\) is any positive machine number strictly less than \(\epsilon\), then \(1+x\) evaluates to \(1\). For standard 64-bit floating-point numbers, the machine epsilon is \(2^{-52}\approx2.22\times10^{-16}\). The practical consequence is that in any floating-point calculation we cannot expect accuracy better than about 15 correct significant figures.
We can use exponential format, also called scientific format, when entering or displaying floating-point numbers. For example, Python will display \(2.3\times10^{17}\) as 2.3e17, and display \(7.3\times10^{-8}\) as 7.3e-08. This format is the most practical if you have to deal with very large or very small numbers.
The binary representation of an integer is quite different from the binary representation of the equivalent float so to the computer 3 looks quite different from 3.0. Nevertheless, if you combine integer and floating-point numbers in arithmetic expressions, then Python will promote each int to its corresponding float to obtain a purely floating-point expression that is then evaluated. For example, 3 + 1/2 becomes 3.0 + 0.5 and then 3.5.
Notice that division is unusual as a binary operation, because the result a/b can have a different type from the operands a and b. If a and b are of type int, then a/b is of type float, even if a/b is a whole number; try the following commands.
8/4
8.0/4.0
In fact, Python provides a second kind of division operation: // carries out integer division. Closely related is the % operator that calculates the remainder. Try evaluating
8//4
8%4
7//4
7%4
Exercise Look up the docstring for the function divmod and use it to find the quotient and remainder when we divide \(3204\) by \(129\).
In mathematics, a complex number consists of a real part and an imaginary part, both of which are real numbers. Similarly, the complex data type in Python consists of two float numbers. Consider the complex number \(z=3+5i\). Python provides two ways to define this number: z = complex(3,5) and z = 3 + 5j. Thus, Python follows the engineering notation j for the imaginary unit. Note, however, that you cannot use j by itself, you have to type 1j. Try the command
1j * 1j
If z is a complex number, then z.real and z.imag give the real and imaginary parts. The abs function is used to find the modulus of a complex number. For example, try
z = 4 - 3j
abs(z)
The angle function gives the phase (polar angle) of a complex number. Try
z = complex(1, sqrt(3))
You can even get the angle in degrees instead of radians by doing
angle(z, deg=True)
We mentioned in Lesson 0 that the NumPy module provides a fast array data type that is essential for many scientific and technical applications. NumPy is an example of an extension module, meaning a Python module that is not actually written in Python but instead uses the C language. In this way, many computationally intensive calculations can be performed 10 to 100 times faster than would be possible running a pure Python program with the standard Python interpreter.
However, a complication is that there are some differences between the way arithmetic works in C from how it works in Python. Type the NumPy-free commands
1/0
1.0/0.0
Both give a ‘ZeroDivisionError’, which is the standard Python behaviour. NumPy defines a float64 data type that behaves like a 64-bit floating point number in C. Type the commands
npzero = float64(0.0)
1/npzero
1.0/npzero
We see that division by npzero produces a RuntimeWarning about division by zero, but does not raise an error. Instead, the expression evaluates to inf, which represents ‘infinity’. Some expressions involving inf evaluate to a finite result. Try the following.
exp(-inf)
arctan(inf)
tanh(-inf)
cos(1/inf)
There are however some arithmetic expressions that cannot be given any meaningful value, either finite or infinite. Try
npzero / npzero
inf - 2*inf
sin(inf)
log(-1.0)
In each case, the result is nan, which stands for ‘not-a-number’. There is nothing magic about inf and nan; both are just particular sequences of 64 bits that are distinct from all of the sequences used to represent ordinary finite floating-point numbers.
The Python math module provides several functions with the same names as NumPy functions. The examples above are all NumPy functions assuming you have set up Spyder to load NumPy in the IPython pane. Try the following.
import math
math.log(-1.0)
math.sin(inf)
In both cases, Python raises a ValueError: math domain error, instead of returning nan.
In this lesson, you have learned about
A classic paper is David Golberg, What every computer scientist should know about floating-point arithmetic, ACM Computing Surveys, Volume 23, pp 5-48, 1991.