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.

- a class instance is dynamic and allows attributes to be added or removed while the program is running,
- the contents of an instance can be inspected by the
`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.