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.

OO

__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)