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
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 radius
en 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.
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()