Now the basic philosophies of object orientation are out of the way, I
am going to discuss how to use object orientation in Python. It starts
with creating new classes using the keyword class
.
class
A class can be considered a new data type. Once a class is created, you
can assign instances of the class to variables. To start simple, I am
going to create a class that represents a point in 2D space. I name this
class Point
(the naming of classes is restricted to the same
requirements as the naming of variables, and it is convention to let the
names of classes start with a capital). Creating this class in Python is
incredibly easy:
class Point:
pass
The keyword pass
in the class definition means “do nothing.” This
keyword can be used wherever you need to place a statement, but you have
nothing yet to place there. You cannot just leave it empty or give a
comment and nothing else. But as soon as statements are added, you no
longer need pass
.
To create an object that is an instance of the class, I assign to a variable the name of the class, with parentheses after it, as if it is a function call (you can have arguments between the parenthesis, which will be discussed a bit later in this chapter).
class Point:
pass
p = Point()
print( type( p ) )
You can obtain information about an object’s class by using the type()
function or __class__
(note __class__
without parentheses).
class Point:
pass
p = Point()
print( p.__class )
With the isinstance
function you can check whether a given object is of a given class.
class Point:
pass
p = Point()
print( isinstance(p,type(p))
print(isinstance("blabla",type(p)))
Of course, a point is more than just an object. A point has an x and a y coordinate. Since Python is a soft-typed language, you need to assign values to attributes to create them. This is done in a special initialization method in the class.
__init__()
The initialization method of a class has the name __init__
(that’s two
underscores, followed by the word init
, followed by two more
underscores). Even if the __init__()
method is not defined explicitly
for the class, it still exists. You use the __init__()
method to
initialize everything that you want to initialize upon creation of an
instance of the class.
In the case of Point
, __init__()
should assure that any Point
object has an x
and a y
coordinate. This is implemented as follows:
class Point:
def __init__( self ):
self.x = 0.0
self.y = 0.0
p = Point()
print( f"({p.x}, {p.y})")
Study the code above closely. You see that __init__()
is defined just
as you would define a function, inside the class definition.
__init__()
gets one parameter, which is called self
. Every method
that you define, always gets at least one parameter, which will get
filled with a reference to the object for which the method is called. By
convention, this first parameter is always called self
. That is not
mandatory, but everybody always does it like this. If you forget to
include that first parameter, you will get a runtime error. If you
forget to include the first parameter self
but you do have other
parameters, Python will fill the first of the parameters that you do
list with a reference to the object, and you will probably also get a
runtime error (as you did not expect that that would happen).
In the __init__()
method for Point
, the object that is created gets
two attributes, which are variables that are part of the object. They
are called x
and y
, and since they are part of the object, you refer
to them as self.x
and self.y
. They both get initial value 0.0, which
makes them floats.
To refer to these attributes when the object has been created, you use
the syntax <object>.<attribute>
, as you can see on the last line of
the code, where the object that has just been created is used in a
print()
statement.
You might wonder if you can only create attributes for an object in the
__init__()
method. The answer is: no, you can create attributes in
other methods too, and even outside the class definition.
class Point:
def __init__( self ):
self.x = 0.0
self.y = 0.0
p = Point()
p.z = 0.0
print( f"({p.x}, {p.y}, {p.z})" )
Most Python programmers (including me) would consider what happens in
the code above bad form. It is good practice to create all the
attributes that you need exclusively in the __init__()
method (though
you can change their values elsewhere), so that you know that every
instance of the class has them, and no instances have more.
Like any method, __init__()
can get arguments. You can use such
arguments to initialize (some of) the attributes. For instance, if I
want to create an instance of Point
while immediately specifying the
values for the x
and y
coordinates, I can use the following class
definition:
class Point:
def __init__( self, x, y ):
self.x = x
self.y = y
p = Point( 3.5, 5.0 )
print( f"({p.x}, {p.y})")
__init__()
is now defined with three parameters. The first is still
self
, as it always has to be there. The second and third are called
x
and y
. I could have called them anything I like (within the
boundaries of variable naming), but I went for x
and y
as these are
the most logical names. I assign x
to self.x
, and y
to self.y
.
I call the creation of a point now with values for the x
and y
coordinates as arguments. The first argument will be passed to the
method in the second parameter, and the second in the third parameter,
as the first parameter will be used to pass the reference to the object
itself.
If you want to make it optional for the programmer to pass such values, you can give the parameters default values using an assignment in the parameter specification, as follows:
class Point:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
p1 = Point()
print( f"({p1.x}, {p2.y})" )
p2 = Point( 3.5, 5.0 )
print( f"({p2.x}, {p2.y})" )
Sometimes we wish certain values to be excluded for one or more attributes of an object. For example, we wish that the coordinates of a point cannot contain negative values. We can achieve this by using the assert instruction of python in the constructor. In the assert statement, we set the conditions to the possible value of the parameter that serves to instantiate a certain attribute. When the assertion is False, executing the constructor causes the program to stop execution with the message that an AssertionError occurred on the line and the message after the comma is stated.
class Point:
def __init__( self, x=0.0, y=0.0 ):
assert x >= 0 and y >= 0, "x coordinate and y coordinate must be greater than or equal to zero".
self.x = x
self.y = y
p = Point(-5,0)
__repr__()
and __str__()
In the code above, I print the point attributes. What happens if I try to print the point itself?
class Point:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
p = Point( 3.5, 5.0 )
print( p )
Try it, and you will agree that the result is not very informative. When
I print a point, I want to see the coordinates. Python offers another
predefined method for that, which is __repr__()
. __repr__()
should
return a string, which contains what you want to see when an object is
displayed.
class Point:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
def __repr__( self ):
return f"({self.x}, {self.y})"
p = Point( 3.5, 5.0 )
print( p )
That looks much better.
Python offers yet another standard method for creating a string version
of an object, namely __str__()
. __str__()
is the same as
__repr__()
, but it is only used when the object is being printed or
passed to a format()
method. If __str__()
is not defined, it is the
same as __repr__()
(but not vice versa). If __str__()
is defined,
you can ensure that something different is shown when print()
is used,
than what is shown in other places.
You now might think: “what other places?” The main “other place” where objects are displayed is in the command shell, when you just type the name of the variable that contains an object.
It is commonly understood that in the __repr__()
method you are
supposed to return a string that contains each and every bit of
information that is needed to recreate an object, while in __str__()
you can just return a string that contains a nicely formatted
representation of the most important information that you want to see in
the program. Very often, these two are the same.
Many programmers ignore __repr__()
altogether and only define
__str__()
. I think this is the wrong way around: you should always
define __repr__()
, while __str__()
is optional. If you use
__repr__()
, make sure that you indeed return all details of an object.
If you leave things out, it is better to just use __str__()
.
__eq__()
Try out the following piece of code:
class Point:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
def __repr__( self ):
return f"({self.x}, {self.y})"
p1 = Point( 3.5, 5.0 )
p2 = Point( 3.5, 5.0 )
p3 = p2
print( p1 == p2 )
print(p2 == p3)
p1 and p2 are two different objects with the same values for the x and y coordinates. The == operator compares the values of p1 and p2 and these are two different references. When we want the == operator to do consider two different objects as the same based on some criterion (in this case: two points are equal if they have the same x and y coordinate), we define in the class definition the method __eq__
. In this method, we first compare whether the two objects are of the same class. If they are not, then False is returned.
class Point:
def __init__( self, x=0.0, y=0.0 ):
self.x = x
self.y = y
def __repr__( self ):
return f"({self.x}, {self.y})"
def __eq__(self,other):
if not isinstance(other,self.__class__):
return False
else:
return self.x == other.x and self.y == other.y
p1 = Point( 3.5, 5.0 )
p2 = Point( 3.5, 5.0 )
p3 == p2
print( p1 == p2 )
print(p2 == p3)