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 methodaspect_ratio
which returns the ratio of its length to its width. - Define a
Circle
class with the radius \(r\) as defining attribute. Implement thearea
andperimeter
class accordingly. - Read and work on the book “Building Skills in Object-Oriented Design” to understand the process of object-oriented design.