Preparation#

Reading material#

This week roughly corresponds to the first 5 sections of Think Python (TP) book Chapter 14 Classes and Functions. We also introduce the concept of methods and the __init__ method, which in the book is covered in Chapter 15 Classes and Methods, sections 1 to 3 and section 6.

Looking at the lecture notes for the CS50 course, this week corresponds to the first two sections of Lecture 8 Object-Oriented Programming.

Copy-and-Run#

The next two weeks are about Object-Oriented Programming (OOP): a programming paradigm where objects are instances of classes, and classes are templates for objects. When defining a class, the programmer can specify the properties of the objects, exactly as it suits the problem at hand. The true benefit of OOP is seen in larger programs.

In small code snippets which we use for Copy-and-run exercises, it can be challenging to come up with examples where OOP is needed. It might seem exaggerated to use classes and objects, for such simple problems. You should keep in mind that the goal of these exercises is to help you understand the basic concepts and Python syntax.

Prep 10.1: Built-in Classes#

Everything you define in Python is an object of some class. When we earlier used the type() we were interested to see which class the object belonged to. Run the code below, and confirm that all objects are of some class.

n = 15
print(type(n))

s = 'Hello'
print(type(s))

d = {'a': 1, 'b': 2}
print(type(d))

l = [1, 2, 3]
print(type(l))

Prep 10.2: Defining Classes#

Run now this code, and see what class the object my_object belongs to.

1class MyClass:
2    pass
3
4my_object = MyClass()
5print(type(my_object))
6print(my_object)

By writing the keyword class followed by the class name and an indented body, you have defined a new class. The class body cannot be empty, so in the code above we used the pass statement that does nothing but serves as a placeholder.

By writing the class name followed by parentheses, you have created an object of the class MyClass. We also say that you have created an instance of the class.

Answer the following questions: On which line (or lines) of the code above do you define a new class? On which line (or lines) do you create an object?

When defining a new class, the programmer specifies how the objects of that class should be printed. In the code above, we did not specify this, so Python uses a default method that prints the class name and the memory address of the object. In the printed output, __main__ means that the class definition is in the file you are currently running.

Run now this code. Look at the print statements in the code, and the order in which they are executed.

class MyClass:
    def __init__(self):
        print('When is this printed? What is self?')

print('    This is before I create an object')
my_object = MyClass()
print('    This is after I create an object')
print(type(my_object))

When defining a new class, the programmer specifies how the objects of that class should be created. For this, the programmer defines a special method with the name __init__, and also called the constructor method. In general, a method is similar to a function, but it is defined inside a class. The __init__ method is executed when an object of the class is created.

In the code above, notice that the constructor method does not use the parameter self. Try removing it and see what happens. Put the self argument back in the constructor method. What does self represent? Try printing self within the the constructor method. What gets printed? Try calling the first parameter something else than self. What happens?

As you can see, self refers to the object itself. In fact, the first parameter of any method always refers to the the object itself!

Prep 10.3: Class Attributes#

Run now this code.

class Person:
    def __init__(self, name):
        self.name = name

a = Person('Anja')
print(type(a))
print(a)
print(a.name)

b = Person('Birgite')
print(type(b))
print(b)
print(b.name)

The constructor method will usually take arguments, which are used to define the object. A value can be assigned to an attribute of the object by using the dot notation.

In the code above, identify the line where the attributes are assigned. Identify also the line where the attributes are accessed.

Run now this code.

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

famous_person = Person('Julius', 'Caesar')
print(famous_person.first_name)
print(famous_person.last_name)

As you can see, the constructor can take multiple arguments.

Add the following line to the constructor method: self.initials = first_name[0] + last_name[0]. Try accessing and printing the attribute initials of the famous_person.

Run the following code.

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        last_name = last_name

famous_person = Person('Julius', 'Caesar')
print(famous_person.first_name)
print(famous_person.last_name)

If you don’o’t assign the attributes to the object, you won’t be able to access them! Therefore, remember to assign attributes that you want to access later.

Now try running the following code

Prep 10.4: More on Class Attributes#

Look at the class definition and the object creation in the code below. Try to predict the output of the code.

class Dog:
    def __init__(self, name, age, race):
        self.name = name
        self.age = age
        self.race = race

my_dog = Dog('Fiddo', 2, 'Border Collie')
print('The age of my dog is', my_dog.age)
my_dog.age = my_dog.age + 1
print("The age of my dog is now", my_dog.age)

Based on the output of the code, answer the question: Is it possible to modify the object attributes from the code outside the class definition?

Now we want to answer the question: Is it possible to add class attributes to an object from the code outside the class definition? To determine this, add the line
my_dog.color = "brown" after the last line in the code, and then try to access and print the attribute color of the object my_dog.

Prep 10.5: Class Methods#

Look at the following code and run it.

class Dog:
    def __init__(self, name, age, race):
        self.name = name
        self.age = age
        self.race = race

    def make_one_year_older(self):
        self.age = self.age + 1

my_dog = Dog('Fiddo', 2, 'Border Collie')
years_to_pass = 5
print('The age of my dog is', my_dog.age)
for year in range(years_to_pass):
    my_dog.make_one_year_older()
    print("The age of my dog is now", my_dog.age)

You can define methods in a class, similar to defining functions outside a class. The first parameter of a method is always self, which is the the object itself. To invoke (execute) a method, you use the dot notation, as in my_dog.make_one_year_older(). The method operates on the object that is before the dot. This object is passed as the first argument to the method.

Notice that we do not reassign the object my_dog, but instead use the method to modify the object. You have seen this behavior when working with lists and dictionaries. This type of operation is called an in-place operation.

Run the following code to see some other examples of class methods.

class Dog:
    def __init__(self, name, age, race):
        self.name = name
        self.age = age
        self.race = race
    
    def make_n_years_older(self, num_years):
        self.age = self.age + num_years
    
    def get_description(self):
        description = f"{self.name} is a {self.age} years old {self.race}."
        return description

my_dog = Dog('Fiddo', 2, 'Border Collie')
print(my_dog.get_description())
my_dog.make_n_years_older(5)
print(my_dog.get_description())

For each of the methods from the class Dog answer the following questions: How many arguments (excluding self) does the method take? What are the types of the arguments passed to the method? What is the data type of the return value of the method?

Look at the slightly modified code below and try to predict the output. Run the code to check your answer.

class Dog:
    def __init__(self, name, age, race):
        self.name = name
        self.age = age
        self.race = race
        self.description = f"{self.name} is a {self.age} years old {self.race}."

    def make_n_years_older(self, num_years):
        self.age = self.age + num_years

my_dog = Dog('Fiddo', 2, 'Border Collie')
print(my_dog.description)
my_dog.make_n_years_older(5)
print(my_dog.description)

Does the code work as intended? What is wrong with the code?

Prep 10.6: Mutability#

Recall that an object is mutable, if you can modify it after it has been created. The simple data types in Python are not mutable: integers, floats, and strings.

Let’s be reminded of this by running the following code. You should be able to predict the output, before running the code.

def add_one(x):
    x = x + 1

x = 5
add_one(x)
print(x)

Look now at the following code. You should be able to predict the output, before running the code.

class MyInteger:
    def __init__(self, value):
        self.value = value

def add_one(x):
    x.value = x.value + 1

x = MyInteger(5)
add_one(x)
print(x.value)

As you can see, you can write a function which modifies the object. Notice that the function has no return statement (it returns None by default). When calling the function, no assignment is needed, as the object is modified in place.

One consequence of mutability is that two variables can refer to the same object. Try to predict the behavior of the following code, before running it.

class Circle:
    def __init__(self, center, radius):
        self.center = center
        self.radius = radius

first_circle = Circle((0, 0), 5)
second_circle = first_circle
print(second_circle.radius)

first_circle.radius = 10
print(second_circle.radius)

Compare with the behavior of the following code, where you use the same class definition as in the example above.

from copy import copy

first_circle = Circle((0, 0), 5)
second_circle = copy(first_circle)
print(second_circle.radius)

first_circle.radius = 10
print(second_circle.radius)

Prep 10.7: ClassNaming#

You have maybe noticed that we name classes differently from functions and variables. For classes we a so-called CapWords (also known as CamelCase) convention. This means that each word in the name starts with a capital letter, and the words are joined without the underscore _. For class methods and attributes, we use the same naming conventions as for functions and variables. Look at the following examples and figure out which are named according to convention.

class officeChair:
    def __init__(self):
        self.num_legs = 4

    def HasLostLeg(self):
        return self.num_legs < 4
class EmperorPenguin:
    def __init__(self, current_place_of_living):
        self.current_place_of_living = current_place_of_living
        self.natural_place_of_living = 'Antarctica'

    def is_perhaps_in_zoo(self,):
        return self.current_place_of_living != self.natural_place_of_living
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    def get_full_name(self, ):
        return self.first_name + " " + self.last_name
class Shipping_Container:
    def __init__(self, maximum_capacity, current_capacity):
        self.MaximumCapacity = maximum_capacity
        self.CurrentCapacity = current_capacity

    def is_full(self):
        return self.CurrentCapacity >= self.MaximumCapacity
        

Prep 10.8: More on Class Methods#

Just like functions, class methods can take arguments of different types, and return values of different types. Run the following code to see some examples.

class Dog:
    def __init__(self, name, age, race):
        self.name = name
        self.age = age
        self.race = race
        self.description = f"The dog is a {self.race}, its name is {self.name} and it is {self.age} years old"

    def is_same_race(self, other):
        return self.race == other.race

    def get_nameless_copy(self):
        return Dog('no name', self.age, self.race)

my_dog = Dog('Fiddo', 2, 'Border Collie')
your_dog = Dog('Spike', 3, 'Bulldog')
print(my_dog.is_same_race(your_dog))

another_dog = my_dog.get_nameless_copy()
print(my_dog.is_same_race(another_dog))

As before, answer the following questions for each of the methods from the class Dog. How many arguments (excluding self) does the method take? What are the types of the arguments passed to the method? What is the data type of the return value of the method?

Self quiz#

Question 10.1#

What is the relationship between the classes and objects in Python?

Question 10.2#

Which keyword is used to define a class in Python?

Question 10.3#

How could you create an object of a class Student?

Question 10.4#

When is the __init__ method executed?

Question 10.5#

What may get printed by the following code?

class MyClass:
    pass
my_object = MyClass()
print(type(my_object))

Question 10.6#

What may get printed by the following code?

class MyClass:
    pass
my_object = MyClass()
print(my_object)

Question 10.7#

In which order are letters A, B, and C printed by the following code?

class MyClass:
    def __init__(self):
        print('A')
print('B')
my_object = MyClass()
print('C')

Question 10.8#

How do you access the attribute age of an object my_dog?

Question 10.9#

Given the following code, how can you change the attribute name of the object my_dog to 'Snap'?

class Dog:
    def __init__(self, name):
        self.name = name
my_dog = Dog('Kvik')

Question 10.10#

What is printed by the following code?

class Box:
    def __init__(self, height):
        self.height = height
my_box = Box(10)
my_box.height = 20
print(my_box.height)

Question 10.11#

What is printed by the following code?

class Box:
    def __init__(self, height):
        self.height = height
    def half_height(self):
        self.height = self.height // 2
my_box = Box(10)
my_box = my_box.half_height()
print(my_box.height)

Question 10.12#

What is printed by the following code?

class Box:
    def __init__(self, height):
        self.height = height
    def half_height(self):
        self.height = self.height // 2
my_box = Box(10)
my_box.half_height()
print(my_box.height)

Question 10.13#

What is printed by the following code?

class Paper:
    def __init__(self, format):
        self.format = format
my_paper = Paper('A3')
your_paper = Paper('A5')
my_paper.format = 'A4'
print(your_paper.format)

Question 10.14#

What is printed by the following code?

class Paper:
    def __init__(self, format):
        self.format = format
my_paper = Paper('A3')
your_paper = my_paper
your_paper.format = 'A4'
my_paper.format = 'A5'
print(your_paper.format)

Question 10.15#

What is the printed by the following code?

class Box:
    def __init__(self, height):
        self.height = height
    def increment_height(self, value):
        self.height = self.height + value
my_box = Box(10)
print(my_box.increment_height(8))

Question 10.16#

What is printed by the following code?

class Box:
    def __init__(self, height):
        self.height = height
    def increment_height(self, value):
        self.height = self.height + value
my_box = Box(10)
my_box.increment_height(8)
print(my_box.height)

Question 10.17#

What is the type of nc in the following code?

class Course:
    def __init__(self, number, year):
        self.number = number
        self.year = year
    def clone(self, year):
        return Course(self.number, year)
my_course = Course('02002', 2024)
nc = my_course.clone(2025)

Question 10.18#

What is the type of x in the following code?

class Course:
    def __init__(self, number, year):
        self.number = number
        self.year = year
    def is_clone(self, other):
        return self.number == other.number
this_course = Course('02002', 2024)
next_course = Course('02002', 2025)
x = this_course.is_clone(next_course)

Question 10.19#

What is printed by the following code?

class Course:
    def __init__(self, number, semester, year):
        self.description = f'{number} {semester} {year}'
this_course = Course('02002', 'Fall', 2024)
print(this_course.description)

Question 10.20#

You want to create a class to represent a shopping list. Which class name would be the most appropriate?