# Classes¶

Especially for larger code bases the rather mathematical way of programming may become slightly convoluted. A paradigm that tries to alleviate this is object-oriented programming. Let us just dive right in with the definition of a rectangle class. Each rectangle has a length and a width, from which one can compute its area and perimeter. Here is the definition:

```
class Rectangle:
"""A rectangle that is described by its length and width."""
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
"""Return the area of the rectangle."""
return self.length*self.width
def perimeter(self):
"""Return the perimeter of the rectangle."""
return 2*(self.length + self.width)
```

Let us go over this step by step. The definition of a class starts with the
keyword `class`

followed by the name of the class. Within the class you can
define *methods*. They are like functions that are attached to the class. What
is slightly peculiar is, that all take `self`

as their first argument—
we will come to that in a second. The first method is `__init__`

.

```
def __init__(self, length, width):
self.length = length
self.width = width
```

As you use classes you *instantiate* *objects* of that class. During the
*instantiation* you can provide some initial arguments to the class to
customize the resulting object using the special `__init__`

method. In this
example a rectangle object can be *initialized* with values for its length
\(l\) and for its width \(w\). These values are then stored in the
*instance* as *attributes*. The reference to the instance is the `self`

argument, that each method has as its first argument. So by attaching
information to `self`

, each distinct object has its own state.

Now we can exploit this in other methods. The area \(A\) of a rectangle is defined as

In the code this is described by the following definition:

```
def area(self):
"""Return the area of the rectangle."""
return self.length*self.width
```

Where a method is defined, that takes the length of the rectangle
`self.length`

, multiplies it with `self.width`

and then returns it. If we
did not use `self.length`

but just `length`

or used `width`

instead of
`self.width`

we would not have accessed the values we stored in this
rectangle during its initialization, but some global values instead. So to make
sure that we only use the attributes of our specific rectangle we access them
from `self`

.

The method used to get the perimeter \(P\), which, for a rectangle, is defined as

follows the same theme:

```
def perimeter(self):
"""Return the perimeter of the rectangle."""
return 2*(self.length + self.width)
```

In the following the usage of this class is shown.

```
first_rectangle = Rectangle(length=2, width=3)
print('Information about the first rectangle')
print('Length:', first_rectangle.length)
print('Width:', first_rectangle.width)
print('Area:', first_rectangle.area())
print('Perimeter:', first_rectangle.perimeter())
```

First an object of the class `Rectangle`

is instantiated with the `length`

argument set to `2`

and the `width`

argument set to `3`

. The name of our
first `Rectangle`

object is `first_rectangle`

. Subsequently the attributes
`length`

and `width`

of the object `first_rectangle`

can be accessed via
`first_rectangle.length`

and `first_rectangle.width`

, respectively. One
could say that what ever has been `self`

in the class definition now is
replaced by the name of the object. In the case of the methods the `self`

argument is implicitly supplied by calling the method from the object. So it is
sufficient to use `first_rectangle.area()`

, and not
`first_rectangle.area(self)`

or `first_rectangle.area(first_rectangle)`

—
both of which would be wrong. The output of the above code is

```
Information about the first rectangle
Length: 2
Width: 3
Area: 6
Perimeter: 10
```

If another rectangle is instantiated with different values the information
changes accordingly. So in the case of a `second_rectangle`

```
second_rectangle = Rectangle(length=5, width=7)
print('Information about the second rectangle')
print('Length:', second_rectangle.length)
print('Width:', second_rectangle.width)
print('Area:', second_rectangle.area())
print('Perimeter:', second_rectangle.perimeter())
```

the output would be

```
Information about the second rectangle
Length: 5
Width: 7
Area: 35
Perimeter: 24
```

## Inheritance¶

A square is a special case of a rectangle, i.e., a rectangle with equal sides. As the computation of the geometrical properties remains the same, one option of initializing a square could be

```
square = Rectangle(length=2, width=2)
```

But maybe you want to be more explicit when initializing squares. This is where
inheritance kicks in. Let us take a look at the definition of a `Square`

class that inherits from the previously defined `Rectangle`

class:

```
class Square(Rectangle):
"""A square that is described by its side length."""
def __init__(self, side_length):
super().__init__(length=side_length, width=side_length)
```

As opposed to the `Rectangle`

class the name of the `Square`

class is
followed by parenthesis containing `Rectangle`

. This tells Python that the
`Rectangle`

class is the *superclass* of `Square`

, i.e., `Square`

inherits from `Rectangle`

. To inherit means that, if not otherwise defined,
`Square`

has the exact same method definitions as its superclass
`Rectangle`

.

But as the `__init__`

method of `Rectangle`

takes the arguments `length`

and `width`

, which is not required for the definition of a square, we can
simplify it. Now it takes only one argument `side_length`

. If we stored it as
we did in the `Rectangle`

class, i.e., as

```
def __init__(self, side_length):
self.side_length = side_length
```

The methods that `Square`

inherits from `Rectangle`

would fail to be
callable, as they access the attributes `length`

and `width`

, which would
not be defined if we took this definition of the `__init__`

method. Instead
we could do this:

```
def __init__(self, side_length):
self.length = side_length
self.width = side_length
```

So now the definition looks awfully similar to the one of the `Rectangle`

class. A bit too similar maybe, and we do not want to repeat ourselves.
Another side-effect is that, should the `__init__`

method of the
`Rectangle`

implement some more code, it would have to be copied to
`Square`

as well. As this is error-prone there is a way to leverage the
method of a superclass within the child class, and this is done using the
`super()`

function. If used within a method definition followed by calling
a method it will resolve to the first parent class that implements a method
with this name and call it for the current object. So by implementing it via

```
def __init__(self, side_length):
super().__init__(length=side_length, width=side_length)
```

we tell Python to call the `__init__`

method of the superclass of `Square`

and pass the side_length for the `length`

and `width`

.

Using the class can now be done like this:

```
first_square = Square(side_length=2)
print('Information about the first square')
print('Length:', first_square.length)
print('Width:', first_square.width)
print('Area:', first_square.area())
print('Perimeter:', first_square.perimeter())
```

The respective output:

```
Information about the first square
Length: 2
Width: 2
Area: 4
Perimeter: 8
```

## Type checking¶

Checking whether an object is of a certain type, or is a child of a certain
type, is done by using the `isinstance()`

function. The first argument is
the object whose type should be checked, the second argument is the class for
which to check.

```
def what_is_it(object):
if isinstance(object, Rectangle):
print('It is a rectangle.')
if isinstance(object, Square):
print('It is a square.')
```

If we use this simple function on our geometrical objects you can see how it works.

```
what_is_it(first_rectangle)
```

As `first_rectangle`

is a `Rectangle`

, but not a `Square`

, the output is:

```
It is a rectangle.
```

Now for the `first_square`

:

```
what_is_it(first_square)
```

As `Square`

is a specialization of `Rectangle`

you can also see that it
identifies as such in addition to being a `Square`

.

```
It is a rectangle.
It is a square.
```

## Special methods¶

Some behavior of Python classes is implemented in terms of so-called Special method names. An example of how they may be used can be seen in the following:

```
import math
class FreeVector:
"""A vector that is not bound by an initial or terminal point."""
def __init__(self, vector):
self.vector = tuple(vector)
@property
def magnitude(self):
return math.sqrt(math.fsum(v**2 for v in self.vector))
@property
def direction(self):
magnitude = self.magnitude
return tuple(v/magnitude for v in self.vector)
def __repr__(self):
return '{self.__class__.__name__}(vector={self.vector!r})'.format(
self=self)
def __str__(self):
return str(self.vector)
def __eq__(self, other):
if (isinstance(other, FreeVector) and
all(math.isclose(a, b) for a, b in zip(
other.vector, self.vector))):
return True
else:
return False
def __neq__(self, other):
return not self.__eq__(self, other)
def __add__(self, other):
if not isinstance(other, FreeVector):
return NotImplemented
return tuple(a + b for a, b in zip(self.vector, other.vector))
def __sub__(self, other):
if not isinstance(other, FreeVector):
return NotImplemented
return tuple(a - b for a, b in zip(self.vector, other.vector))
```

The usage may be as follows:

```
>>> a = FreeVector((1, 2, 3))
>>> a
FreeVector(vector=(1, 2, 3))
>>> str(a)
'(1, 2, 3)'
>>> b = FreeVector((1, 2, 3))
>>> c = FreeVector((4, 5, 6))
>>> a == b
True
>>> a == c
False
>>> a + c
(5, 7, 9)
>>> c - a
(3, 3, 3)
```

## Exercises¶

- Copy the definition of the
`Rectangle`

class and extend it by adding a method`aspect_ratio`

which returns the ratio of its length to its width. - Define a
`Circle`

class with the radius \(r\) as defining attribute. Implement the`area`

and`perimeter`

class accordingly. - Read and work on the book “Building Skills in Object-Oriented Design” to understand the process of object-oriented design.