At some point, you will have to work with numbers, so we start by considering different forms of numeric types in Python. In mathematics, we distinguish between natural numbers (ℕ), integers (ℤ), rational numbers (ℚ), real numbers (ℝ) and complex numbers (ℂ). These are infinite sets of numbers. Operations differ between these sets and may even not be defined. For example, the usual division of two numbers in ℤ might not result in an integer — it is not defined on ℤ.
In Python, like many other computer languages, we have numeric types:
int
, which is at least theoretically the entire ℤfloat
, which is a finite subset of ℝ andcomplex
, which is a finite subset of ℂFinite sets have a smallest and a largest number and there is a minimum spacing between two numbers; refer to the section on Floating Point Representation for further details.
The simplest numerical type is the integer type.
The statement k = 3
assigns the variable k
to an integer.
Applying an operation of the type +
, -
, or *
to integers returns an integer. The division operator, //
, returns an integer, while /
may return a float:
6 // 2 # 3 7 // 2 # 3 7 / 2 # 3.5
The set of integers in Python is unbounded; there is no largest integer. The limitation here is the computer’s memory rather than any fixed value given by the language.
If you execute the statement a = 3.0
in Python, you create a floating-point number (Python type: float
). These numbers form a subset of rational numbers, ℚ.
Alternatively the constant could have been given in exponent notation as a = 30.0e-1
or simply a = 30.e-1
. The symbol e
separates the exponent from the mantissa, and the expression reads in mathematical notation a = 30.0 × 10−1. The name floating-point number refers to the internal representation of these numbers and reflects the floating position of the decimal point when considering numbers over a wide range.
Applying the elementary mathematical operations +
, -
, *
, and /
to two floating-point numbers or to an integer and a floating-point number returns a floating-point number. Operations between floating-point numbers rarely return the exact result expected from rational number operations:
0.4 - 0.3 # returns 0.10000000000000003
This facts matters, when comparing floating point numbers:
0.4 - 0.3 == 0.1 # returns False
Internally, floating-point numbers are represented by four quantities: the sign, the mantissa, the exponent sign, and the exponent:
with β ϵ ℕ and x0≠ 0, 0 ≤ xi≤ β
x0...xt-1 is called the mantissa, β the basis and e the exponent |e| ≤ U . t is called the mantissa length. The condition x0 ≠ 0 makes the representation unique and saves, in the binary case (β = 2), one bit.
There exist two-floating point zeros +0 and -0, both represented by the mantissa 0.
On a typical Intel processor, β = 2 . To represent a number in the float
type 64 bits are used, namely 2 bits for the signs, t = 52 bits for the mantissa and 10 bits for the exponent |e|
. The upper bound U for the exponent is consequently 210-1 = 1023.
With this data the smallest positive representable number is
flmin = 1.0 × 2-1023 ≈ 10-308 and the largest is flmax = 1.111...1 × 21023 ≈ 10308.
Note that floating-point numbers are not equally spaced in [0, flmax]. There is in particular a gap at zero (refer to [29]). The distance between 0 and the first positive number is 2-1023, while the distance between the first and the second is smaller by a factor 2-52≈ 2.2 × 10-16. This effect, caused by the normalization x0 ≠ 0, is visualized in Figure 2.1.
This gap is filled equidistantly with subnormal floating-point numbers to which such a result is rounded. Subnormal floating-point numbers have the smallest possible exponent and do not follow the convention that the leading digit x0 has to differ from zero; refer to [13].
There are in total floating-point numbers. Sometimes a numerical algorithm computes floating-point numbers outside this range.
This generates number over- or underflow. In SciPy the special floating-point number inf
is assigned to overflow results:
exp(1000.) # inf a = inf 3 - a # -inf 3 + a # inf
Working with inf
may lead to mathematically undefined results. This is indicated in Python by assigning the result another special floating-point number, nan
. This stands for not-a-number, that is, an undefined result of a mathematical operation:
a + a # inf a - a # nan a / a # nan
There are special rules for operations with nan
and inf
. For instance, nan
compared to anything (even to itself) always returns False
:
x = nan x < 0 # False x > 0 # False x == x # False
See Exercise 4 for some surprising consequences of the fact that nan
is never equal to itself.
The float inf
behaves much more as expected:
0 < inf # True inf <= inf # True inf == inf # True -inf < inf # True inf - inf # nan exp(-inf) # 0 exp(1 / inf) # 1
One way to check for nan
and inf
is to use the isnan
and isinf
functions. Often, one wants to react directly when a variable gets the value nan
or inf
. This can be achieved by using the NumPy command seterr
. The following command
seterr(all = 'raise')
would raise an error if a calculation were to return one of those values.
Underflow occurs when an operation results in a rational number that falls into the gap at zero; refer to Figure 2.1.
Figure 2.1: The floating point gap at zero, here t = 3, U = 1
The machine epsilon or rounding unit is the largest number ε such that float(1.0 + ε) = 1.0.
Note that ε ≈ β1-t/2 = 1.1102 × 10-16 on most of today’s computers. The value that is valid on the actual machine you are running your code on is accessible using the following command:
import sys sys.float_info.epsilon # 2.220446049250313e-16 (something like that)
The variable sys.float_info
contains more information about the internal representation of the float type on your machine.
The function float
converts other types to a floating-point number—if possible. This function is especially useful when converting an appropriate string to a number:
a = float('1.356')
NumPy also provides other float types, known from other programming languages as double-precision and single-precision numbers, namely float64
and float32
:
a = pi # returns 3.141592653589793 a1 = float64(a) # returns 3.1415926535897931 a2 = float32(a) # returns 3.1415927 a - a1 # returns 0.0 a - a2 # returns -8.7422780126189537e-08
The second last line demonstrates that a
and a1
do not differ in accuracy. In the first two lines, they only differ in the way they are displayed. The real difference in accuracy is between a
and its single-precision counterpart, a2
.
The NumPy function finfo
can be used to display information on these floating-point types:
f32 = finfo(float32) f32.precision # 6 (decimal digits) f64 = finfo(float64) f64.precision # 15 (decimal digits) f = finfo(float) f.precision # 15 (decimal digits) f64.max # 1.7976931348623157e+308 (largest number) f32.max # 3.4028235e+38 (largest number) help(finfo) # Check for more options
Complex numbers are an extension of the real numbers frequently used in many scientific and engineering fields.
Complex numbers consist of two floating-point numbers, the real part a of the number and its imaginary part b. In mathematics, a complex number is written as z=a+bi, where i defined by i2 = -1 is the imaginary unit. The conjugate complex counterpart of z is .
If the real part a is zero, the number is called an imaginary number.
In Python, imaginary numbers are characterized by suffixing a floating-point number with the letter j
, for example, z = 5.2j
. A complex number is formed by the sum of a floating-point number and an imaginary number, for example, z = 3.5 + 5.2j
.
While in mathematics the imaginary part is expressed as a product of a real number b with the imaginary unit i, the Python way of expressing an imaginary number is not a product: j
is just a suffix to indicate that the number is imaginary.
This is demonstrated by the following small experiment:
b = 5.2 z = bj # returns a NameError z = b*j # returns a NameError z = b*1j # is correct
The method conjugate
returns the conjugate of z
:
z = 3.2 + 5.2j z.conjugate() # returns (3.2-5.2j)
One may access the real and imaginary parts of a complex number z using the real
and imag
attributes. Those attributes are read-only:
z = 1j z.real # 0.0 z.imag # 1.0 z.imag = 2 # AttributeError: readonly attribute
It is not possible to convert a complex number to a real number:
z = 1 + 0j z == 1 # True float(z) # TypeError
Interestingly, the real
and imag
attributes as well as the conjugate method work just as well for complex arrays (Chapter 4, Linear Algebra – Arrays). We demonstrate this by computing the Nth roots of unity which are , that is, the N solutions of the equation :
N = 10 # the following vector contains the Nth roots of unity: unity_roots = array([exp(1j*2*pi*k/N) for k in range(N)]) # access all the real or imaginary parts with real or imag: axes(aspect='equal') plot(unity_roots.real, unity_roots.imag, 'o') allclose(unity_roots**N, 1) # True
The resulting figure (Figure 2.2) shows the roots of unity together with the unit circle. (For more details on how to make plots, refer Chapter 6, Plotting.)
Figure 2.2: Roots of unity together with a unit circle
It is of course possible to mix the previous methods, as illustrated by the following examples:
z = 3.2+5.2j (z + z.conjugate()) / 2. # returns (3.2+0j) ((z + z.conjugate()) / 2.).real # returns 3.2 (z - z.conjugate()) / 2. # returns 5.2j ((z - z.conjugate()) / 2.).imag # returns 5.2 sqrt(z * z.conjugate()) # returns (6.1057350089894991+0j)
3.12.123.189