18. Complex type, I¶
Up to this point, we have mostly ignored the more mathematical type for complex numbers. We have waited until now, because they are simpler to use when we have a grasp of applying methods.
18.1. Complex type¶
In mathematics, the set of complex numbers includes real and imaginary numbers, as well as their sum, where the "imaginary number" is defined as . (There is a divide in the mathematical sciences -- many people use to denote the imaginary number, but we use here because that is what Python syntax uses. Congratulations, engineers!) In general, a complex number number can be written as the sum of a real component and an imaginary component as , where (and we stress that the "imaginary component" is itself a real number). Each component can be found using the following functions: and . So, a real number is just a special case of a complex number with , and an imaginary number is one where .
We can picture such numbers on a 2D complex plane (or Argand diagram). The horiontal axis is real and the vertical axis is imaginary . Thus, a complex point is given by the coordinate pair , such that the real component provides the "x" coordinate and the imaginary component, the "y" coordinate.
We can translate these to Python using the type which is evocatively named complex. Note that not every programming language has such a type; in others, you have consider the real and imaginary components as forming a short array (mimicking a vector with 2 components, say). This will be a useful picture to keep in mind at times, but we can also avail ourselves of Python's type/class and related functionality.
In Python, we denote a complex type by including an imaginary-looking
term, which is a number followed by immediately by the letter j
,
with no space in between. Thus, the following are all recognized as
complex by Python (you can use type()
on each to verify it):
c1 = -2 + 3j # nonzero real and imag components
c2 = 3j - 2 # order of components does not matter
c3 = 34.1 - 1j # more nonzero real and imag components
c4 = 5 + 0j # a complex with zero imag component
c5 = 0 + 9j # a complex with zero real component
c6 = 9j # same as above: complex with zero real component
c7 = complex(4) # arg by position; imag=0 by default
c8 = complex(5,6) # args by position, both real and imag set
c9 = complex() # no args; real=0 and imag=0 by default
... which would print, respectively, to (comments added):
(-2+3j) # c1
(-2+3j) # c2
(34.1-1j) # c3
(5+0j) # c4
9j # c5
9j # c6
(4+0j) # c7
(5+6j) # c8
0j # c9
Some comments:
For Python to recognize "j" as the imaginary number, it must always be preprended by a number. If we write
1+j
, Python will treatj
as a variable that must have been defined above; we would have to write1+1j
to denote a complex number (assuming we hadn't defined a complex-valued variablej
previously...).Note that
c5
andc6
are both of type complex, as there is no separate "imaginary" type in Python; by including aj
on one number, the result will be a complex, whether or not there is real term added in.When there is a nonzero real component, the result is written in parentheses. This allows us to have expected behavior when multiplying by a constant: mathematically, . The following evaluate very differently:
(34.1 - 1j)*5
vs34.1 - 1j*5
.To have a complex type, there must be an imaginary component, even if it is zero.
c4
is complex, whilec4b = 5
would be int.In defining
c7
,c8
, andc9
, we used the built-incomplex()
function with different numbers of args. We can read the help docstring viacomplex?
to understand the default input values depending on how any args we enter:1Init signature: complex(real=0, imag=0) 2Docstring: 3Create a complex number from a real part and an optional imaginary part. 4 5This is equivalent to (real + imag*1j) where imag defaults to 0. 6Type: type 7Subclasses: complex128
Using basic mathematical operators with complex numbers just follows
the mathematical rules. For example, c2 + c3
evaluates to
(32.1+2j)
, which is the expected sum of real and imaginary
components.
The magnitude and phase of a complex number are important concepts, forming a complementary pair of real values to describe it (the polar form, as opposed to the rectangular form above): . These can also be viewed on the complex plane, where is essentially the hypotenuse of the triangle formed by the sides and from the origin, and is the angle of this hypotenuse line the positive real () part of the horizontal axis. One can relate the rectangular and polar coordinate pairs mathematically:
... and we could translate these definitions to computational forms. Or, since they are pretty fundamental quantities, we could see if things exist in Python to generate the values from the complex directly. And there are, noting that "magnitude" is the same as the absolute value and "phase" is an angle:
print(np.real(c1))
print(np.imag(c1))
print(np.abs(c1))
print(np.angle(c1))
... which output:
-2.0
3.0
3.605551275463989
2.158798930342464
And of course, we shouldn't just guess that the outputs are correct
and/or what we want-- we should verify them. The real and imaginary
parts are straightforward to verify by inspection, and we translate
the mathematical expressions above to verify the polar values. In
particular, we will also want to know if the angle value is in radians
or degrees; looking at part of the help string for it (np.angle?
):
... we see that: 1) the output value is in radians by default, and 2) we can control this with a kwarg.
If we want to initialize an array of complex numbers, we can use all we have learned so far about arrays, and just extend it to having a complex datatype:
arr_comp = np.zeros(5, dtype=complex)
print(arr_comp)
... produces:
[0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]
One can have arrays and sequence of complex numbers; consider for example the following mathematical sequence (recognizing as the imaginary number here, not an index, based on the syntax!):
Q: Use a for-loop to create the above array of values in Python.
+ show/hide codeQ: Make an array c_real
of and
c_imag
of for all , and plot
these. You have just made an Argand diagram, plotting on the complex
plane.
18.2. Complex type methods¶
With our new found appreciation for methods that go with each
type/class, let's look at some for the complex type with
help(complex)
:
1Help on class complex in module builtins:
2
3class complex(object)
4 | complex(real=0, imag=0)
5 |
6 | Create a complex number from a real part and an optional imaginary part.
7 |
8 | This is equivalent to (real + imag*1j) where imag defaults to 0.
9 |
10 | Methods defined here:
11 |
12 | __abs__(self, /)
13 | abs(self)
14 |
15 | __add__(self, value, /)
16 | Return self+value.
17 |
18 | __bool__(self, /)
19 | self != 0
20...
21
22 | conjugate(...)
23 | complex.conjugate() -> complex
24 |
25 | Return the complex conjugate of its argument. (3-4j).conjugate() == 3+4j.
26 |
27...
28
29 | Data descriptors defined here:
30 |
31 | imag
32 | the imaginary part of a complex number
33 |
34 | real
35 | the real part of a complex number
Some of these look familiar from our early adventures into methods,
such as __abs__()
and __add()
. Though, as we noted before,
these methods are specific to this class/type, so they just happen
to have the same name.
The absolute value (AKA magnitude) is pretty interesting. For a complex number, we have a mathematical definition in terms of real and imaginary components as . If we try this method out using one of the above variables:
print(c1.__abs__())
... we see that the result 3.605551275463989
is indeed what we
would expect from the component values, running the check by
evaluating ((-2)**2 + 3**2)**0.5
.
Looking further down the methods list, we also see conjugate
.
This shares the name of the important mathematical operation for
complex numbers, where the complex conjugate of is defined
as . Testing out this complex class method,
we see that:
print(c1.conjugate())
... evaluates according to our expectation: (-2-3j)
. Phew.
Finally, we don't see a list of attributes in the docstring, but we do see a list of data descriptors that looks kind of similar (i.e., like variables inside the type/class). Descriptors are different than attributes, and that differentiation is deeper, internal Python topic than we will cover here. But from a practical "usage" point of view, we can typically think of them as synonymous to attributes and use them similarly when we see them in docstring descriptions. So, let's do so here:
print(c1.real)
print(c1.imag)
... produces:
-2.0
3.0
That looks correct, based on the value assigned to the complex. (It
also matches with the functional values above from np.real()
and
np.imag()
.
18.3. Practice¶
Evaluate
(17 - 4j).real + 1
.Evaluate the square of the complex number
5+3j
.What type are each of the real and imag components of a complex in Python? Is it always the same, e.g., whether one has
1+2j
,1.0+2j
or1+2.0j
? What does this imply about testing equality with either a full complex or any of its components?