Objects#

Learning goals

After finishing this chapter, you are expected to be able to

  • understand what objects are

  • write basic Python classes

  • define attributes and methods

  • understand class inheritance

Object-oriented programming#

Python is an object-oriented programming language. That means that we primarily (and actually, only) work with objects. Everything in Python is an object. We can define objects for anything. Without knowing it, you have been using objects for the past few weeks. For example, a list is an object in Python, a NumPy array is an object, an integer is an object, etc. An object is a variable that has attributes (variables) and methods (functions). This sounds abstract, but is actually very convenient.

You can thing of an object as a ‘thing’ that has some properties that are just values (attributes) and some functions that do something with/on the object (methods). An attribute is simply a variable with a value. A method is very much like a function: it can have input parameters and can return something.

For example, last week you have worked with NumPy arrays. A NumPy ndarray is a class that has

  • attributes, e.g.,

    • shape

    • size

  • methods, e.g.,

    • sort()

    • max()

    • sum()

Any object of the same type will have the same attributes and methods, but can have different values for its attributes. When we’re running

import numpy as np

some_array = np.array([1, 2, 3])

we’re actually creating an instance of the class ndarray. We can verify that by using type (as we’ve done earlier to find that variables are integers, floats, etc.

print(type(some_array))
<class 'numpy.ndarray'>

We can make another array, so that we have two objects of the same class:

some_other_array = np.array([[1, 2],
                             [2, 4]])

Obviously other_array and some_other_array are not the same variable, but they do have the same type:

print(type(some_other_array))
<class 'numpy.ndarray'>

Both some_array and some_other_array are instances of the same class, namely the ndarray class in NumPy, and we can use all attributes and methods defined for that class on either of the instances, as below.

print(some_array.shape)
print(some_other_array.shape)
(3,)
(2, 2)
print(some_array.sum())
print(some_other_array.sum())
6
9

Objects in Python

Everything in Python is an object!

Whole books have been written about object-oriented programming. Here, we’re just going to show you some basics so you understand how Python handles this topic and you learn some new syntax.

Classes#

So, how do we define a type or class? And what actually is a class? A class in Python is a data type for a specific type of object. You can consider it a blueprint for making objects. For example, we can have a Car class or Bike class. In itself, such a class has no information, but when we make an instance of the class we can add specific information. Consider the example of a Car class: we can add attributes (variables) to each individual instance of a car, and define methods (functions) for such a car that we can call.

The most basic Car class would look as follows:

class Car:
    pass

Let’s break this down. By using class we tell Python that we’re defining a class. Furthermore, we provide the name of the class (here Car), and end with a : (as we do for def, if, for etc.). The second line starts with indentation, and we here use pass (as we can do for functions) as we have not yet defined the rest of the class.

Class naming conventions

If you like CAPITALS, your patience will finally be rewarded. In Python, naming conventions for classes are different than for variables and functions. Class names are written in camelcase and without underscores, i.e., ClassName vs. function_name or variable_name.

Attributes#

We can extend our class by giving it attributes, or object variables. For example, let’s say that a car always has a maximum speed of 180 km/h. Then we can define a variable within the class that we call max_speed and give it a value. This is called a class attribute because its value will be the same for any object we make with this class.

class Car:
    max_speed = 180

We can add other attributes as well. For example, if we assume that a car will always have four wheels, we can create a class attribute num_wheels = 4.

class Car:
    max_speed = 180
    num_wheels = 4

Constructor method#

Any class will also need methods. These are functions that are defined to operate on the class. The constructor of a class is called when a new instance of a class (an object) is initialized. In Python, the constructor method should always be named __init__. We define methods using def, just like we do for functions. If we add a constructor method to our Car class, it could look like below. The __init__ method here has two parameters: self and brand. A method should always have self as an argument as it refers to the object. Our second parameter brand is just something that we define for this specific class. In the body of the __init__ method, we use this parameter to set an instance attribute: the brand will be used to set the brand of the car itself. The instance attribute is indicated with self.brand.

class Car:
    max_speed = 180
    num_wheels = 4
    
    def __init__(self, brand):
        self.brand = brand

init

Now let’s use this new class. We can create objects for different cars now, with different brands. Let’s first create a single Skoda car. When we run the line below, a new object of class Car is made and its __init__ function is called with brand="Skoda". Note how we don’t provide self as an argument here: that happens under the hood.

current_car = Car(brand="Skoda")

If we now check the type of current_car, we find that it has class Car.

print(type(current_car))
<class '__main__.Car'>

If we want to know what the brand is of our car, we can just print its brand attribute.

print('The car is a {}'.format(current_car.brand))
The car is a Skoda

Other methods#

In addition to the __init__ method, other methods can be defined for a class. For example, for the Car class we can define a method that toots the claxon.

class Car:
    max_speed = 180
    num_wheels = 4
    
    def __init__(self, brand):
        self.brand = brand
        
    def toot(self, times):
        for _ in range(times):
            print('Toot!')

We can call this method at any time once an object of this class has been created. For example, we can create a car of brand Suzuki, and then use the claxon five times by calling the method toot with times=5.

previous_car = Car("Suzuki")
previous_car.toot(5)
Toot!
Toot!
Toot!
Toot!
Toot!

Exercise 7.1

Write your answers in a Jupyter notebook file, separate exercises with headers in Markdown, as you have seen in Chapter 6.

Define a class for a circle, call it Circle. As attributes, give it an instance attribute radius and a method calculate_area that should return the area of the circle.

Test your new class by making multiple circles with different radii.

Opdracht 7.1

Schrijf je antwoorden op in een Jupyter notebook. Scheid de vragen met Markdown headers zoals je hebt gezien in Hoofdstuk 6.

Definieer een class voor een cirkel, noem deze Circle. Geef de class een instance attribute radiusen een method calculate_area die als output de oppervlakte van de cirkel geeft.

Probeer je class uit door meerdere cirkels te maken met verschillende radii.

Class inheritance#

There are natural connections between many classes, and there often is a hierarchy between classes. For example, cars and bikes are both vehicles, so it would make sense if we can make a parent class from which we use parts in our cars and bikes classes. This is called inheritance. Using inheritance, we can define a class that inherits all properties and methods from another class. We call the class that inherits the child class or the derived class and the class from which we inherit the parent class or base class. You can think of a derived class as a specialized version of the base class.

inheritance

For example, we could have a parent class that defines a person, and a more specialized child class that defines a student. This works because a student is always a person, but the reverse does not necessarily hold (a person is not always a student). We can first define - in the same was as before - our Person class. You have already learned that you can create an object of this class by invoking the __init__ function, i.e., x = Person("Frenk", "Simanos"). Now, we have created an object that corresponds to a person, but not yet to a student.

class Person:
    def __init__(self, fname, lname):
        self.firstname = fname
        self.lastname = lname

    def print_name(self):
        print("I am a person and my name is {} {}".format(self.firstname, self.lastname))

#Use the Person class to create an object, and then execute the printname method:
x = Person("Frenk", "Simanos")
x.print_name()
I am a person and my name is Frenk Simanos

The derived class of this would have a first name and last name as well. In the simplest case, we can make a derived class as follows. The parentheses after Student indicate that this class inherits from Person.

class Student(Person):
    pass

We can create a new student now, just like we created a person before.

x = Student("Harry", "Potter")
x.print_name()
I am a person and my name is Harry Potter

The child class has inherited the properties of the parent class, including it’s __init__ and print_name methods. We can override the __init__ method of the parent class by defining a new __init__ method in the derived class. Within that method, you can first call the __init__ method of the parent class, in this case to set the first name and last name of the student.

class Student(Person):
    def __init__(self, fname, lname):
        Person.__init__(self, fname, lname)   # You could here also use super() instead of Person

Moreover, you can extend the __init__ method, and add additional attributes. For example, to add a study program to the student, we just adapt the __init__ method of the Student class (but not the Person class) and set an attribute.

class Student(Person):
    def __init__(self, fname, lname, program):
        Person.__init__(self, fname, lname)   
        self.program = program
        
x = Student("Harry", "Potter", "sorcery")

We can also override the original print_name method, or define completely new methods.

class Student(Person):
    def __init__(self, fname, lname, program):
        Person.__init__(self, fname, lname)   
        self.program = program
    
    def print_name(self):         # Overrides method of Person
        print("I am a person and my name is {} {}. I am a student in {}.".format(self.firstname, self.lastname, self.program))
    
    def print_program(self):      # New method specific to Student
        print(self.program)
        
x = Student("Harry", "Potter", "sorcery")
x.print_name()
I am a person and my name is Harry Potter. I am a student in sorcery.

If you think about it, every class - including the Person class that we defined - inherits from other classes. In essence, everything in Python is an object, and any new class that you define inherits from Python’s object class. Even though we never explicitly define them, the Person class has a number of methods that we can override. These can be listed using the built-in dir function.

dir(Person)
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'print_name']

Python puts no restrictions on the level of inheritance that you use, and we can in fact use nested inheritance where a new derived class inherits from a class that is itself derived from a base class. In our example, we could define a derived class for bachelor students, i.e.

class BachelorStudent(Student):
    def __init__(self, fname, lname, program, year):
        Student.__init__(self, fname, lname, program)
        self.year = year
        
    def printname(self):
        print(self.firstname, self.lastname, self.program, self.year)

x = BachelorStudent("Laila", "Koll", "TM", 1)
x.printname()
Laila Koll TM 1

Exercise 7.2

Design a class for your favorite animal. The class should inherit from the base class Animal:

class Animal:
    
    def __init__(self, name):
        self.sort_name = name

Animals should be able to do at least the following, implemented in appropriate methods

  • make a sound characteristic for that animal

  • eat their favorite food

  • tell you their geographic origin

In your answer, include code that calls these methods.

Opdracht 7.2

Ontwerp een class voor je favoriete dier. Deze class moet overerven uit de basis class Animal:

class Animal:
    
    def __init__(self, name):
        self.sort_name = name

Dieren moeten in staat zijn om ten minste het volgende te doen, geïmplementeerd in de geschikte methods

  • een karakteristiek geluid maken

  • hun favoriete voedsel eten

  • zeggen waar ze vandaan komen

Maak een code die all deze methodes aanroept.

Een voorbeeld:

class Tiger(Animal):
    
    def __init__(self, color):
        super().__init__("tiger")
        self.color = color
    
    def make_sound(self):
        print("Roaaar")
        
    def eat_food(self):
        print("I'm eating tofu")
        
    def show_origin(self):
        print("I live in India")
        

tijgetje = Tiger("orange")
tijgetje.make_sound()
tijgetje.eat_food()
tijgetje.show_origin()