Note: Click on "Kernel" > "Restart Kernel and Clear All Outputs" in JupyterLab before reading this notebook to reset its output. If you cannot run this file on your machine, you may want to open it in the cloud .
In the third part of this chapter, we first look at the lesser known complex
type. Then, we introduce a more abstract classification scheme for the numeric types.
complex
Type¶What is the solution to x2=−1 ?
Some mathematical equations cannot be solved if the solution has to be in the set of the real numbers R. For example, x2=−1 can be rearranged into x=√−1, but the square root is not defined for negative numbers. To mitigate this, mathematicians introduced the concept of an imaginary number i that is defined as i=√−1 or often as the solution to the equation i2=−1. So, the solution to x=√−1 then becomes x=i.
If we generalize the example equation into (mx−n)2=−1⟹x=1m(√−1+n) where m and n are constants chosen from the reals R, then the solution to the equation comes in the form x=a+bi, the sum of a real number and an imaginary number, with a=nm and b=1m.
Such "compound" numbers are called complex numbers , and the set of all such numbers is commonly denoted by C. The reals R are a strict subset of C with b=0. Further, a is referred to as the real part and b as the imaginary part of the complex number.
Complex numbers are often visualized in a plane like below, where the real part is depicted on the x-axis and the imaginary part on the y-axis.
complex
numbers are part of core Python. The simplest way to create one is to write an arithmetic expression with the literal j
notation for i. The j
is commonly used in many engineering disciplines instead of the symbol i from math as I in engineering more often than not means electric current .
For example, the answer to x2=−1 can be written in Python as 1j
like below. This creates a complex
object with value 1j
. The same syntactic rules apply as with the above e
notation: No spaces are allowed between the number and the j
. The number may be any int
or float
literal; however, it is stored as a float
internally. So, complex
numbers suffer from the same imprecision as float
numbers.
x = 1j
id(x)
140084727448400
type(x)
complex
x
1j
To verify that it solves the equation, let's raise it to the power of 2.
x ** 2 == -1
True
Often, we write an expression of the form a+bi.
2 + 0.5j
(2+0.5j)
Alternatively, we may use the complex() built-in: This takes two parameters where the second is optional and defaults to
0
. We may either call it with one or two arguments of any numeric type or a str
object in the format of the previous code cell without any spaces.
complex(2, 0.5)
(2+0.5j)
By omitting the second argument, we set the imaginary part to 0.
complex(2)
(2+0j)
The arguments to complex() may be any numeric type or properly formated
str
object.
complex("2+0.5j")
(2+0.5j)
Arithmetic expressions work with complex
numbers. They may be mixed with the other numeric types, and the result is always a complex
number.
c1 = 1 + 2j
c2 = 3 + 4j
c1 + c2
(4+6j)
c1 - c2
(-2-2j)
c1 + 1
(2+2j)
3.5 - c2
(0.5-4j)
5 * c1
(5+10j)
c2 / 6
(0.5+0.6666666666666666j)
c1 * c2
(-5+10j)
c1 / c2
(0.44+0.08j)
A complex
number comes with two attributes .real
and .imag
that return the two parts as float
objects on their own.
x.real
0.0
x.imag
1.0
Also, a .conjugate()
method is bound to every complex
object. The complex conjugate is defined to be the complex number with identical real part but an imaginary part reversed in sign.
x.conjugate()
-1j
The cmath module in the standard library
implements many of the functions from the math
module such that they work with complex numbers.
Analogous to the discussion of containers and iterables in Chapter 4 , we contrast the concrete numeric data types in this chapter with the abstract ideas behind numbers in mathematics
.
The figure below summarizes five major sets of numbers in mathematics as we know them from high school:
In the listed order, the five sets are perfect subsets of the respective following sets, and C is the largest set. To be precise, all sets are infinite, but they still have a different number of elements.
The data types introduced in this chapter and its appendix are all (im)perfect models of abstract mathematical ideas.
The int
and Fraction
types are the models "closest" to the idea they implement: Whereas Z and Q are, by definition, infinite, every computer runs out of bits when representing sufficiently large integers or fractions with a sufficiently large number of decimals. However, within a system-dependent range, we can model an integer or fraction without any loss in precision.
For the other types, in particular, the float
type, the implications of their imprecision are discussed in detail above.
The abstract concepts behind the four outer-most mathematical sets are formalized in Python since PEP 3141 in 2007. The numbers
module in the standard library
defines what programmers call the numerical tower
, a collection of five abstract data types
, or abstract base classes (ABCs) as they are called in Python jargon:
Number
: "any number" (cf., documentation Complex
: "all complex numbers" (cf., documentation Real
: "all real numbers" (cf., documentation Rational
: "all rational numbers" (cf., documentation Integral
: "all integers" (cf., documentation import numbers
dir(numbers)
['ABCMeta', 'Complex', 'Integral', 'Number', 'Rational', 'Real', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'abstractmethod']
The primary purpose of ABCs is to classify the concrete data types and standardize how they behave. This guides us as the programmers in what kind of behavior we should expect from objects of a given data type. In this context, ABCs are not reflected in code but only in our heads.
For, example, as all numeric data types are Complex
numbers in the abstract sense, they all work with the built-in abs() function (cf., documentation
). While it is intuitively clear what the absolute value
(i.e., "distance" from 0) of an integer, a fraction, or any real number is, abs()
calculates the equivalent of that for complex numbers. That concept is called the magnitude
of a number, and is really a generalization of the absolute value.
Relating back to the concept of duck typing mentioned in Chapter 4 ,
int
, float
, and complex
objects "walk" and "quack" alike in context of the abs() function.
abs(-1)
1
abs(-42.87)
42.87
The absolute value of a complex
number x is defined with the Pythagorean Theorem where ‖x‖=√a2+b2 and a and b are the real and imaginary parts.
abs(3 + 4j)
5.0
On the contrary, only Real
numbers in the abstract sense may be rounded with the built-in round() function.
round(123, -2)
100
round(42.1)
42
Complex
numbers are two-dimensional. So, rounding makes no sense here and leads to a TypeError
. So, in the context of the round() function,
int
and float
objects "walk" and "quack" alike whereas complex
objects do not.
round(1 + 2j)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[29], line 1 ----> 1 round(1 + 2j) TypeError: type complex doesn't define __round__ method
Another way to use ABCs is in place of a concrete data type.
For example, we may pass them as arguments to the built-in isinstance() function and check in which of the five mathematical sets the object
1 / 10
is.
isinstance(1 / 10, float)
True
A float
object is a generic Number
in the abstract sense but may also be seen as a Complex
or Real
number.
isinstance(1 / 10, numbers.Number)
True
isinstance(1 / 10, numbers.Complex)
True
isinstance(1 / 10, numbers.Real)
True
Due to the float
type's inherent imprecision, 1 / 10
is not a Rational
number.
isinstance(1 / 10, numbers.Rational)
False
However, if we model 1 / 10
as a Fraction
object (cf., Appendix ), it is recognized as a
Rational
number.
from fractions import Fraction
isinstance(Fraction("1/10"), numbers.Rational)
True
Replacing concrete data types with ABCs is particularly valuable in the context of "type checking:" The revised version of the factorial()
function below allows its user to take advantage of duck typing: If a real but non-integer argument n
is passed in, factorial()
tries to cast n
as an int
object with the int() built-in.
Two popular and distinguished Pythonistas, Luciano Ramalho and Alex Martelli
, coin the term goose typing to specifically mean using the built-in isinstance()
function with an ABC (cf., Chapter 11 in this book or this summary thereof).
def factorial(n, *, strict=True):
"""Calculate the factorial of a number.
Args:
n (int): number to calculate the factorial for; must be positive
strict (bool): if n must not contain decimals; defaults to True;
if set to False, the decimals in n are ignored
Returns:
factorial (int)
Raises:
TypeError: if n is not an integer or cannot be cast as such
ValueError: if n is negative
"""
if not isinstance(n, numbers.Integral):
if isinstance(n, numbers.Real):
if n != int(n) and strict:
raise TypeError("n is not integer-like; it has non-zero decimals")
n = int(n)
else:
raise TypeError("Factorial is only defined for integers")
if n < 0:
raise ValueError("Factorial is not defined for negative integers")
elif n == 0:
return 1
return n * factorial(n - 1)
factorial()
works as before, but now also accepts, for example, float
numbers.
factorial(0)
1
factorial(3)
6
factorial(3.0)
6
With the keyword-only argument strict
, we can control whether or not a passed in float
object may come with decimals that are then truncated. By default, this is not allowed and results in a TypeError
.
factorial(3.1)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[41], line 1 ----> 1 factorial(3.1) Cell In[37], line 19, in factorial(n, strict) 17 if isinstance(n, numbers.Real): 18 if n != int(n) and strict: ---> 19 raise TypeError("n is not integer-like; it has non-zero decimals") 20 n = int(n) 21 else: TypeError: n is not integer-like; it has non-zero decimals
In non-strict mode, the passed in 3.1
is truncated into 3
resulting in a factorial of 6
.
factorial(3.1, strict=False)
6
For complex
numbers, factorial()
still raises a TypeError
because they are neither an Integral
nor a Real
number.
factorial(1 + 2j)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[43], line 1 ----> 1 factorial(1 + 2j) Cell In[37], line 22, in factorial(n, strict) 20 n = int(n) 21 else: ---> 22 raise TypeError("Factorial is only defined for integers") 24 if n < 0: 25 raise ValueError("Factorial is not defined for negative integers") TypeError: Factorial is only defined for integers