This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.
Imagine that Python did not already have complex numbers. We could then make a class for such numbers and support the standard mathematical operations. This exercise turns out to be a very good pedagogical example of programming with classes and special methods, so we shall make our own class for complex numbers and go through all the details of the implementation.
The class must contain two data attributes: the real and imaginary part of the complex number. In addition, we would like to add, subtract, multiply, and divide complex numbers. We would also like to write out a complex number in some suitable format. A session involving our own complex numbers may take the form
>>> u = Complex(2,-1)
>>> v = Complex(1) # zero imaginary part
>>> w = u + v
>>> print w
(3, -1)
>>> w != u
True
>>> u*v
Complex(2, -1)
>>> u < v
illegal operation "<" for complex numbers
>>> print w + 4
(7, -1)
>>> print 4 - w
(1, 1)
We do not manage to use exactly the same syntax with j
as
imaginary unit as in Python's built-in complex numbers so
to specify a complex number we must create a Complex
instance.
Here is the complete implementation of our class for complex numbers:
class Complex(object):
def __init__(self, real, imag=0.0):
self.real = real
self.imag = imag
def __add__(self, other):
return Complex(self.real + other.real,
self.imag + other.imag)
def __sub__(self, other):
return Complex(self.real - other.real,
self.imag - other.imag)
def __mul__(self, other):
return Complex(self.real*other.real - self.imag*other.imag,
self.imag*other.real + self.real*other.imag)
def __div__(self, other):
sr, si, or, oi = self.real, self.imag, \
other.real, other.imag # short forms
r = float(or**2 + oi**2)
return Complex((sr*or+si*oi)/r, (si*or-sr*oi)/r)
def __abs__(self):
return sqrt(self.real**2 + self.imag**2)
def __neg__(self): # defines -c (c is Complex)
return Complex(-self.real, -self.imag)
def __eq__(self, other):
return self.real == other.real and self.imag == other.imag
def __ne__(self, other):
return not self.__eq__(other)
def __str__(self):
return '(%g, %g)' % (self.real, self.imag)
def __repr__(self):
return 'Complex' + str(self)
def __pow__(self, power):
raise NotImplementedError\
('self**power is not yet impl. for Complex')
The special methods for addition, subtraction, multiplication,
division, and the absolute value
follow easily from the mathematical definitions of these
operations for complex
numbers.
What -c
means when c
is of type Complex
,
is also easy to
define and implement. The __eq__
method needs a word of
caution: the method is mathematically correct, but comparison of real numbers
on a computer should always employ a tolerance.
The version of __eq__
shown above is about compact
code and equivalence to the mathematics. Any real-world numerical
computations should employ a test that
abs(self.real - other.real) < eps
and
abs(self.imag - other.imag) < eps
, where eps
is some
small tolerance, say eps = 1E-14
.
The final __pow__
method exemplifies a way to
introduce a method in a class, while we postpone its implementation. The simplest
way to do this is by inserting an empty function body using the
pass
("do nothing") statement:
class Polynomial(object):
...
def __pow__(self, power):
# Postpone implementation of self**power
pass
However, the preferred method is to raise a NotImplementedError
exception so that users writing power expressions are notified that
this operation is not available. The simple pass
will just
silently bypass this serious fact!
Some mathematical operations, like the comparison operators
>
, >=
, etc., do not have a meaning for complex numbers.
By default, Python allows us to use these comparison operators
for our Complex
instances, but the boolean result will be
mathematical nonsense.
Therefore, we should implement the corresponding special methods and
give a sensible error message that the operations are not available
for complex numbers. Since the messages are quite similar, we
make a separate method to gather common operations:
def _illegal(self, op):
print 'illegal operation "%s" for complex numbers' % op
Note the underscore prefix: this is a Python convention telling
that the _illegal
method is local to the class in the sense
that it is not supposed to be used outside the class, just by other
class methods.
In computer science terms, we say that names starting with an underscore
are not part of the application programming interface, known as
the API.
Other programming languages, such as Java, C++, and C#, have special
keywords, like private
and protected
that can be used
to technically hide both data and methods from users of the class.
Python will never restrict anybody who tries to access data or
methods that are considered private to the class, but the leading underscore
in the name reminds any user of the class that she now touches parts
of the class that are not meant to be used "from the outside".
Various special methods for comparison operators can now call
up _illegal
to issue the error message:
def __gt__(self, other): self._illegal('>')
def __ge__(self, other): self._illegal('>=')
def __lt__(self, other): self._illegal('<')
def __le__(self, other): self._illegal('<=')
The implementation of class Complex
is far from perfect.
Suppose we add a complex number and a real number, which is
a mathematically perfectly valid operation:
w = u + 4.5
This statement leads to an exception,
AttributeError: 'float' object has no attribute 'real'
In this case, Python sees u + 4.5
and tries to
use u.__add__(4.5)
, which causes trouble because
the other
argument in the __add__
method
is 4.5, i.e., a float
object, and float
objects
do not contain an attribute with the name real
(other.real
is used in our __add__
method, and accessing
other.real
is what causes the error).
One idea for a remedy could be to set
other = Complex(other)
since this construction turns a real number other
into a
Complex
object. However, when we add two Complex
instances, other
is of type Complex
, and
the constructor simply stores this Complex
instance as
self.real
(look at the method __init__
).
This is not what we want!
A better idea is to test for the type of other
and perform the
right conversion to Complex
:
def __add__(self, other):
if isinstance(other, (float,int)):
other = Complex(other)
return Complex(self.real + other.real,
self.imag + other.imag)
We could alternatively drop the conversion of other
and
instead implement two addition rules, depending on
the type of other
:
def __add__(self, other):
if isinstance(other, (float,int)):
return Complex(self.real + other, self.imag)
else:
return Complex(self.real + other.real,
self.imag + other.imag)
A third way is to look for what we require from the other
object,
and check that this demand is fulfilled. Mathematically, we require
other
to be a complex or real number, but from a programming
point of view, all we demand (in the original __add__
implementation) is that other
has real
and
imag
attributes. To check if an object a
has an attribute
with name stored in the string attr
, one can use the function
hasattr(a, attr)
In our context, we need to perform the test
if hasattr(other, 'real') and hasattr(other, 'imag'):
Our third implementation of the __add__
method therefore becomes
def __add__(self, other):
if isinstance(other, (float,int)):
other = Complex(other)
elif not (hasattr(other, 'real') and \
hasattr(other, 'imag')):
raise TypeError('other must have real and imag attr.')
return Complex(self.real + other.real,
self.imag + other.imag)
The advantage with this third alternative is that we may add
instances of class Complex
and Python's own complex class (complex
),
since all we need is an object with real
and imag
attributes.
The presentations of alternative implementations of the
__add__
actually touch some very important computer science
topics. In Python, function arguments can refer to
objects of any type, and the type of an argument can change
during program execution. This feature is known as dynamic typing
and supported by languages such as Python, Perl, Ruby, and Tcl.
Many other languages, C, C++, Java, and C# for instance, restrict
a function argument to be of one type, which must be known when we write
the program. Any attempt to call the function with an argument of another
type is flagged as an error. One says that the language employs
static typing, since the type cannot change as in languages
having dynamic typing. The code snippet
a = 6 # a is integer
a = 'b' # a is string
is valid in a language with dynamic typing, but not in a language with static typing.
Our next point is easiest illustrated through an example. Consider the code
a = 6
b = '9'
c = a + b
The expression a + b
adds an integer and a string, which is
illegal in Python. However, since b
is the string '9'
,
it is natural to interpret a + b
as 6 + 9
.
That is, if the string b
is converted to an integer, we may
calculate a + b
. Languages performing this conversion automatically
are said to employ weak typing, while languages that require
the programmer to explicit perform the conversion, as in
c = a + float(b)
are known to have strong typing. Python, Java, C, and C# are
examples of languages with strong typing, while Perl and C++ allow
weak typing. However, in our third
implementation of the __add__
method,
certain types - int
and float
–
are automatically converted to the right type Complex
.
The programmer has therefore imposed a kind of weak typing in the
behavior of the addition operation for complex numbers.
There is also something called duck typing where the code only
imposes a requirement of some data or methods in the object, rather
than demanding the object to be of a particular type. The explanation
of the term duck typing is the principle: if it walks like a duck,
and quacks like a duck, it's a duck. An operation a + b
may be
valid if a
and b
have certain properties that make it possible to
add the objects, regardless of the type of a
or b
. To enable
a + b
in our third implementation of the __add__
method, it is
sufficient that b
has real
and imag
attributes. That is,
objects with real
and imag
look like Complex
objects. Whether
they really are of type Complex
is not considered important in this
context.
There is a continuously ongoing debate in computer science which kind of typing that is preferable: dynamic versus static, and weak versus strong. Static and strong typing, as found in Java and C#, support coding safety and reliability at the expense of long and sometimes repetitive code, while dynamic and weak typing support programming flexibility and short code. Many will argue that short code is more readable and reliable than long code, so there is no simple conclusion.
What happens if we add a float
and a Complex
in that order?
w = 4.5 + u
This statement causes the exception
TypeError: unsupported operand type(s) for +: 'float' and 'instance'
This time Python cannot find any definition of what the plus operation
means with a float
on the left-hand side and a Complex
object
on the right-hand side of the plus sign.
The float
class was created many years ago without any
knowledge of our Complex
objects, and we are not allowed
to extend the __add__
method
in the float
class to handle
Complex
instances. Nevertheless, Python has a special method
__radd__
for the case where the class instance (self
)
is on the right-hand side of the operator and the
other
object is on the left-hand side. That is, we may implement
a possible float
or int
plus a Complex
by
def __radd__(self, other): # defines other + self
return self.__add__(other) # other + self = self + other
Similar special methods exist for subtraction, multiplication, and
division. For the subtraction operator, observe that other - self
,
which is the operation assumed to implemented
in __rsub__
, can be realized by
other.__sub__(self)
. A possible implementation is
def __sub__(self, other):
print 'in sub, self=%s, other=%s' % (self, other)
if isinstance(other, (float,int)):
other = Complex(other)
return Complex(self.real - other.real,
self.imag - other.imag)
def __rsub__(self, other):
print 'in rsub, self=%s, other=%s' % (self, other)
if isinstance(other, (float,int)):
other = Complex(other)
return other.__sub__(self)
The print
statements are inserted to better understand how these
methods are visited. A quick test demonstrates what happens:
>>> w = u - 4.5
in sub, self=(2, -1), other=4.5
>>> print w
(-2.5, -1)
>>> w = 4.5 - u
in rsub, self=(2, -1), other=4.5
in sub, self=(4.5, 0), other=(2, -1)
>>> print w
(2.5, 1)
As you probably realize, there is quite some code to be implemented
and lots of considerations to be resolved before we have a class
Complex
for professional use in the real world. Fortunately,
Python provides its complex
class, which offers everything we need
for computing with complex numbers. This fact reminds us that it is
important to know what others already have implemented, so that we
avoid "reinventing the wheel".
In a learning process, however, it is a probably a very
good idea to look into
the details of a class Complex
as we did above.
The purpose of this section is to explain how we can easily look at the contents of a class instance, i.e., the data attributes and the methods. As usual, we look at an example - this time involving a very simple class:
class A(object):
"""A class for demo purposes."""
def __init__(self, value):
self.v = value
def dump(self):
print self.__dict__
The self.__dict__
attribute is briefly mentioned in
the section Making classes without the class construct.
Every instance is automatically equipped with this attribute, which
is a dictionary that stores all the ordinary attributes of the instance
(the variable names are keys, and the object references are values).
In class A
there is only one data attribute, so the
self.__dict__
dictionary contains one key, 'v'
:
>>> a = A([1,2])
>>> a.dump()
{'v': [1, 2]}
Another way of inspecting what an instance a
contains is to call
dir(a)
. This Python function
writes out the names of all methods and variables
(and more) of an object:
>>> dir(a)
'__doc__', '__init__', '__module__', 'dump', 'v']
The __doc__
variable
is a docstring, similar to docstrings in
functions,
i.e., a description of the class
appearing as a first string right after the class
headline:
>>> a.__doc__
'A class for demo purposes.'
The __module__
variable holds the name of the module in
which the class is defined. If the class is defined in the program itself and
not in an imported module, __module__
equals
'__main__'
.
The rest of the entries in the list returned
from dir(a)
correspond to attribute names defined by
the programmer of the class, in this example the method attributes
__init__
and dump
, and
the data attribute v
.
Now, let us try to add new variables to an existing instance:
>>> a.myvar = 10
>>> a.dump()
{'myvar': 10, 'v': [1, 2]}
>>> dir(a)
['__doc__', '__init__', '__module__', 'dump', 'myvar', 'v']
The output of a.dump()
and dir(a)
show that we were
successful in adding a new variable to this instance on the fly.
If we make
a new instance, it contains only the variables and methods that
we find in the definition of class A
:
>>> b = A(-1)
>>> b.dump()
{'v': -1}
>>> dir(b)
['__doc__', '__init__', '__module__', 'dump', 'v']
We may also add new methods to an instance, but this will not be shown here.
Adding or removing attributes may sound scary and highly illegal to C, C++, and Java programmers, but more dynamic classes is natural and legal in many other languages - and often useful.
dir
function,
and the data attributes are available through the __dict__
dictionary.inspect
, doing more detailed inspection
of Python objects. One can, for example, get the arguments of
functions or methods and even inspect the code of the object.