5. Toward Object-Oriented Programming

In this chapter and the next, we will implement a kinematic simulation of parabolic motion, as shown in the animation below. Through this example, you will learn how to define classes as well as the basic concepts of object-oriented programming.

  • Left click: throw a particle that disappears at the screen boundary
  • Right click: project a particle that bounces at the screen boundary
  • Esc key: Exit

5.1. An Empty Framework

First, let’s create a framework for the entire program, which opens a black window, loops at 60 frames/s, and is stopped with the pygame.QUIT event or the Esc key.

Create a folder named particle in the projects folder, and open the folder in VSCode. Create a file named particle.py in the folder and open it. You should see particle.py - particle - VScodium (Computer Seminar I) in the title bar of VSCode.

Then, enter the following code.

particle.py (ver 1.0)
 1import pygame
 2
 3
 4def main():
 5    pygame.init()
 6    width, height = 600, 400
 7    screen = pygame.display.set_mode((width, height))
 8    clock = pygame.time.Clock()
 9
10    while True:
11        frames_per_second = 60
12        clock.tick(frames_per_second)
13
14        should_quit = False
15        for event in pygame.event.get():
16            if event.type == pygame.QUIT:
17                should_quit = True
18            elif event.type == pygame.KEYDOWN:
19                if event.key == pygame.K_ESCAPE:
20                    should_quit = True
21        if should_quit:
22            break
23
24        screen.fill(pygame.Color("black"))
25        pygame.display.update()
26
27    pygame.quit()
28
29
30if __name__ == "__main__":
31    main()

If you understand the content up to the previous chapter, there is no need to explain anything. Because this chapter takes a different approach to abstraction than simply refactoring out functions, we put everything in one main function first.

Open the Source Control sidebar, initialize the Git repository, and commit the contents of the file. While developing the following, make sure to commit changes when appropriate.

5.2. Parabolic Motion

The velocity \(\boldsymbol{v}(t)\) and position \(\boldsymbol{x}(t)\) of a particle that is accelerated at \(\boldsymbol{a}(t)\) can be written as follows.

\[\begin{split}\frac{d \boldsymbol{v}}{dt} &= \boldsymbol{a}\\ \frac{d \boldsymbol{x}}{dt} &= \boldsymbol{v}\end{split}\]

In order to simulate this on a computer, we discretize the time in terms of \(\Delta t\) and introduce the following approximation.

\[\begin{split}\Delta \boldsymbol{v} &= \boldsymbol{a} \Delta t\\ \Delta \boldsymbol{x} &= \boldsymbol{v} \Delta t\end{split}\]

Let’s use the left mouse button click to set the initial position where the initial velocity is randomly generated. We assume a constant gravity acceleration where y-axis is the direction of gravity.

We will use the standard Python function random.uniform to generate a random number. random.uniform(a, b) generates a uniform random number in the range a to b.

particle.py (ver 2.0)
 1import random
 2import pygame
 3
 4
 5def main():
 6    pygame.init()
 7    width, height = 600, 400
 8    screen = pygame.display.set_mode((width, height))
 9    clock = pygame.time.Clock()
10
11    dt = 1.0
12    gy = 0.5
13    particle_is_alive = False
14    x, y = 0, 0
15    vx, vy = 0, 0
16
17    while True:
18        frames_per_second = 60
19        clock.tick(frames_per_second)
20
21        should_quit = False
22        for event in pygame.event.get():
23            if event.type == pygame.QUIT:
24                should_quit = True
25            elif event.type == pygame.KEYDOWN:
26                if event.key == pygame.K_ESCAPE:
27                    should_quit = True
28            elif event.type == pygame.MOUSEBUTTONDOWN:
29                if event.button == 1:
30                    particle_is_alive = True
31                    x, y = event.pos
32                    vx = random.uniform(-10, 10)
33                    vy = random.uniform(-10, 0)
34        if should_quit:
35            break
36
37        if particle_is_alive:
38            vy += gy * dt
39            x += vx * dt
40            y += vy * dt
41
42        screen.fill(pygame.Color("black"))
43        if particle_is_alive:
44            radius = 10
45            pygame.draw.circle(screen, pygame.Color("green"), (x, y), radius)
46        pygame.display.update()
47
48    pygame.quit()
49
50
51if __name__ == "__main__":
52    main()
Line 1
The module random is imported.
Lines 13-15
We assume that the particle does not exist until the first left click. If the variable particle_is_alive is False, no motion calculation or drawing is performed. We also initialize the positional coordinates x, y, and the velocities vx, vy with appropriate values.
Lines 28-33
We detect an Event type object whose type attribute is pygame.MOUSEBUTTONDOWN. In this case, the button attribute of the Event type object contains the mouse button number. The left button number is 1.
Lines 37-40, 43-45
Motion calculation and drawing are performed only when particle_is_alive is True.
Wasn’t the left button set to 0 in the previous chapter?
Yes, this is a weird thing about pygame.

In the previous chapter we used it as an index into the sequence returned by pygame.mouse.get_pressed. Recall that the index of a sequence starts at 0. The left button is 0, the middle button (if exists) is 1, and the right button is 2.

By contrast, event.button is not an index, so it doesn’t have to start at 0… I don’t know if that’s why, but the left button is 1, the middle button is 2, and the right button is 3. Annoying.

Isn’t the order of calculation of velocity and position wrong?
This is intentional. This is called a symplectic integrator.

There are various schemes of integrating the equations of motion in discrete time, each of which introduces different errors. For long time calculations, it is particularly important that the solution does not diverge with time. The symplectic integrator is known for its simplicity and its relatively low energy growth.

The main point of the Symplectic integrator is to update the velocity first, and then use the updated velocity to update the position. The same can be interpreted as updating the position first, evaluating the occurrence of a force at that position, and then updating the velocity with the acceleration caused by that force. (In the current example, the force is only gravitational acceleration, which is always constant, so there is not much benefit. It will come into play in future extensions)


If you click the left button repeatedly for a short time, you will see that the current particle disappears and a new one is created immediately when you click the left button. This is a bit unnatural. Let’s prohibit creation of a new one until the current one goes off the screen.

particle.py (ver 3.0)
21        should_quit = False
22        for event in pygame.event.get():
23            if event.type == pygame.QUIT:
24                should_quit = True
25            elif event.type == pygame.KEYDOWN:
26                if event.key == pygame.K_ESCAPE:
27                    should_quit = True
28            elif event.type == pygame.MOUSEBUTTONDOWN:
29                if event.button == 1 and not particle_is_alive:
30                    particle_is_alive = True
31                    x, y = event.pos
32                    vx = random.uniform(-10, 10)
33                    vy = random.uniform(-10, 0)
34        if should_quit:
35            break
36
37        if particle_is_alive:
38            vy += gy * dt
39            x += vx * dt
40            y += vy * dt
41            if x < 0 or x > width or y > height:
42                particle_is_alive = False
43
44        screen.fill(pygame.Color("black"))

If you just remember to use the operators and, or, and not for the boolean operations used in the if statement, you should have no difficulty.


To take this a step further, let’s consider moving multiple particles simultaneously.

A quick way to do this is to make a list of x, y, vx, vy, and particle_is_alive. For example, the motion calculation for the k-th particle might look like this:

vy[k] += gy * dt
x[k] += vx[k] * dt
y[k] += vy[k] * dt

This is possible, but if the number of particles increases and decreases frequently, it is cumbersome to keep track of all the elements in the list that need to be appended and deleted. If you accidentally forget one of them (e.g., if you forget to append only particle_is_alive), the indices will shift among variables, leading to a bug that is difficult to identify.

Instead, it is more reasonable to treat a single particle as a single piece of data, and use x, vx, etc. as “belonging to it”. In the following, we will adopt such a policy.

5.3. Putting the Data Together

In short, what we are going to do is to create an object of an original type with attributes such as x and vx. The mechanism that defines the type of an object, i.e., what attributes and operations are possible on the object, is called a class.

Let’s actually define a Particle class that represents a particle. We’ll start with just one particle.

particle.py (ver 4.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    pass
 7
 8
 9def main():
10    pygame.init()
11    width, height = 600, 400
12    screen = pygame.display.set_mode((width, height))
13    clock = pygame.time.Clock()
14
15    dt = 1.0
16    gy = 0.5
17    p = Particle()
18    p.is_alive = False
19    p.x, p.y = 0, 0
20    p.vx, p.vy = 0, 0
21
22    while True:
23        frames_per_second = 60
24        clock.tick(frames_per_second)
25
26        should_quit = False
27        for event in pygame.event.get():
28            if event.type == pygame.QUIT:
29                should_quit = True
30            elif event.type == pygame.KEYDOWN:
31                if event.key == pygame.K_ESCAPE:
32                    should_quit = True
33            elif event.type == pygame.MOUSEBUTTONDOWN:
34                if event.button == 1 and not p.is_alive:
35                    p.is_alive = True
36                    p.x, p.y = event.pos
37                    p.vx = random.uniform(-10, 10)
38                    p.vy = random.uniform(-10, 0)
39        if should_quit:
40            break
41
42        if p.is_alive:
43            p.vy += gy * dt
44            p.x += p.vx * dt
45            p.y += p.vy * dt
46            if p.x < 0 or p.x > width or p.y > height:
47                p.is_alive = False
48
49        screen.fill(pygame.Color("black"))
50        if p.is_alive:
51            radius = 10
52            pygame.draw.circle(screen, pygame.Color("green"), (p.x, p.y), radius)
53        pygame.display.update()
54
55    pygame.quit()
56
57
58if __name__ == "__main__":
59    main()
Lines 5-6

A class named Particle is defined. Don’t forget the colon : after the class name; you’re already somewhat familiar with Python, so when you see the colon, you should be able to guess that the next line will be indented by one level. That’s right.

However, there is nothing to be written at this new indentation level. In Python, if there is something that needs to be written grammatically, but there is essentially nothing to write, we put a pass statement. You may remember that it is a statement that does nothing.

It may seem strange that there is nothing to write. But the fact that we created a class named Particle is enough here.

Lines 17-20

By writing p = Particle(), we create an object of type Particle and assign it to the variable p.

Then, we define the attributes x, y, vx, vy, and is_alive of the object pointed to by p. Since the attributes are defined here, only pass is needed in line 6.

An object of type Particle is also called an instance of type Particle or an instance of class Particle. Creating an object is also called instantiating it.

Lines 34-38, 42-47
Through the attributes of the variable p, we set the initial position and velocity, calculate the motion, and draw the object.
The usage of p = Particle() looks similar to such as those of pygame.Color and pygame.time.Clock.
Yes, also in their usages, the class name is followed by () to generate the object. Note that pygame.Color takes an argument in (); the explanation of how it works will be given soon.
I think classes are similar to structs in the C language, but with structs in C, we have to enumerate member variables such as x and vx when defining them. In Python classes, why can we just pass?
Classes are indeed similar to structs in C.

In C, when we write

struct Particle {
    float x;
    float y;
    float vx;
    float vy;
    int is_alive;
};

you can create a new type called struct Particle, and the data of that type will have member variables x, y, vx, yv, and is_alive. A class is an extension of a struct that can have functions (methods) in addition to variables as members. Class is also provided in other languages such as C++ and Java.

In C, C++, Java, and other languages, variables must be declared in advance, so member variables must be declared when defining structs and classes. In Python, by contrast, variables do not need to be declared, so there is no need to list them when defining a class.

Function members (methods) will be added to the class class sson. Functions must be defined before use also in Python, so we can’t just pass them.

Unlike variable names and function names, we should capitalize the beginning of a word like Particle, right?
Yes, it’s a Python class naming convention. If it’s a multi-word name, we write it like UpperCamelCase.

This kind of naming is called UpperCamelCase or PascalCase. When the first letter is lowercased, it is called lowerCamelCase. (If people simply say “camel case”, it depends on which they mean by that)

Are objects and instances the same thing?
In most cases, they refer to almost the same thing, but with slightly different meanings.

In the first place, an instance means a particular or concrete example. Therefore, the statement “x is an instance” does not make sense (leading to the response “Instance of … what?”). It only makes sense to say “x is an instance of class A”.

However, the statement “x is an object” can make sense on its own; in Python, you can say “Oh, x has attributes and methods” or “x appears on the right side when you visualize it with the Online Python Tutor”.

(The following is a bit complicated, so don’t worry about it until you get used to using Python)

The statement “x is an object of class A” is not wrong, but it is better to say “x is an object of type A” or “x is an object that is an instance of class A” to avoid misunderstandings. The reason for this is that in Python, a class is also a kind of object.


Now, let’s extend it to simulate multiple particles simultaneously. We create a list of Particle objects, particle_list, to manage multiple particles.

particle.py (ver 5.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    pass
 7
 8
 9def main():
10    pygame.init()
11    width, height = 600, 400
12    screen = pygame.display.set_mode((width, height))
13    clock = pygame.time.Clock()
14
15    dt = 1.0
16    gy = 0.5
17    particle_list = []
18
19    while True:
20        frames_per_second = 60
21        clock.tick(frames_per_second)
22
23        should_quit = False
24        for event in pygame.event.get():
25            if event.type == pygame.QUIT:
26                should_quit = True
27            elif event.type == pygame.KEYDOWN:
28                if event.key == pygame.K_ESCAPE:
29                    should_quit = True
30            elif event.type == pygame.MOUSEBUTTONDOWN:
31                if event.button == 1:
32                    p = Particle()
33                    p.is_alive = True
34                    p.x, p.y = event.pos
35                    p.vx = random.uniform(-10, 10)
36                    p.vy = random.uniform(-10, 0)
37                    particle_list.append(p)
38        if should_quit:
39            break
40
41        for p in particle_list:
42            p.vy += gy * dt
43            p.x += p.vx * dt
44            p.y += p.vy * dt
45            if p.x < 0 or p.x > width or p.y > height:
46                p.is_alive = False
47
48        particle_list[:] = [p for p in particle_list if p.is_alive]
49
50        screen.fill(pygame.Color("black"))
51        for p in particle_list:
52            radius = 10
53            pygame.draw.circle(screen, pygame.Color("green"), (p.x, p.y), radius)
54        pygame.display.update()
55
56    pygame.quit()
57
58
59if __name__ == "__main__":
60    main()
Line 17
The particle_list variable is prepared. It is initialized with an empty list.
Lines 31-37

When the mouse button is pressed, a Particle type object is created, its attributes are initialized, and it is appended to the particle_list.

To allow the existence of multiple particles, the condition “if is_alive” is removed from the if statement.

Line 41
This line calculates the motion of each element in particle_list and checks if it is out of range or not.
Line 48
This removes the particles whose is_alive attribute is False. Using the list comprehension notation, it creates a list with only those elements that have the is_alive attribute set to True, and replaces all elements in particle_list with the new list.
Line 51
Each element of particle_list is drawn.
I’m not familiar with the list comprehension notation, so I’m having trouble understanding the deletion part. Isn’t it possible to write it in a more normal way than this Python specific notation?
There are other ways to do it, but list comprehensions are probably the easiest to understand.

In the following, we’ll discuss other ways, but I think you’ll probably say “Oh, screw it, I get it, I’ll just use the list comprehensions”. Then feel free to stop reading there.

The following are some of the simplest wayss that we can think of:

for p in particle_list:
    if not p.is_alive:
        particle_list.remove(p)

But this will result in an error. It is not allowed to modify the elements of a list while it is being passed around in a for statement.

To get around this, the following technique is often used:

for p in particle_list[:]:
    if not p.is_alive:
        particle_list.remove(p)

Did you see what has changed? The list used in the for statement is now particle_list[:]. This avoid the above limitation by making a copy of particle_list.

This has a subtle issue in terms of performance. The for statement loops over each element in the list while removing elements, but particle_list.remove(p) includes the process of searching for p from the front of the list. This is a loop inside a loop, but since the element to be removed is already identified, there should be no need for such a double loop.

To avoid this, you may think that you can use the index to specify the element to be removed:

for i in range(len(particle_list)):
    if not particle_list[i].is_alive:
        del particle_list[i]

However, this does not work as expected. If the elements of particle_list are deleted from the front, the indices for the elements after the deleted ones will be changed. Therefore, the correct way is to reverse the result of range and use:

for i in reverse(range(len(particle_list))):
    if not particle_list[i].is_alive:
        del particle_list[i]

See, it’s easier to understand the list comprehension?

In line 48, why is it assigned to a slice? Can’t I just write particle_list = ...?
In this example, either is fine, but note that they work slightly differently. In the later chapters, we will see examples where it is important to assign to a slice.

With particle_list = ..., the new list created on the right hand side will be re-assigned to the particle_list variable. The list originally assigned to particle_list will be discarded.

By contrast, with particle_list[:] = ..., the contents of the list originally assigned to particle_list will be replaced by the contents of the right hand side list.

5.4. Organizing Data Manipulation

We succeeded in moving multiple particles simultaneously where a set of data related to each particle is put together in an object. In the meantime, the main function is getting long. It suggests that putting the data together is not enough to make the code concise. We also need to refactor the processing into functions as we did before.

However, putting the data together is not a waste of time. Refactoring the code can be more effective if you consider the data structure as well as the processing flow.

Specifically, we employ the following policy:

  • From the main program side, treat a particle as a single entity as much as possible; functions are defined to confine the code that directly reads and writes individual attributes.
  • If it is difficult to completely confine the code manipulating attributes, the main program side is allowed to read individual attributes; however, avoid to write whenever possible.
particle.py (ver 6.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    pass
 7
 8
 9def init_particle(p, pos, vel):
10    p.is_alive = True
11    p.x, p.y = pos
12    p.vx, p.vy = vel
13
14
15def update_particle(p, width, height, dt, gy):
16    p.vy += gy * dt
17    p.x += p.vx * dt
18    p.y += p.vy * dt
19    if p.x < 0 or p.x > width or p.y > height:
20        p.is_alive = False
21
22
23def draw_particle(p, screen):
24    radius = 10
25    pygame.draw.circle(screen, pygame.Color("green"), (p.x, p.y), radius)
26
27
28def main():
29    pygame.init()
30    width, height = 600, 400
31    screen = pygame.display.set_mode((width, height))
32    clock = pygame.time.Clock()
33
34    dt = 1.0
35    gy = 0.5
36    particle_list = []
37
38    while True:
39        frames_per_second = 60
40        clock.tick(frames_per_second)
41
42        should_quit = False
43        for event in pygame.event.get():
44            if event.type == pygame.QUIT:
45                should_quit = True
46            elif event.type == pygame.KEYDOWN:
47                if event.key == pygame.K_ESCAPE:
48                    should_quit = True
49            elif event.type == pygame.MOUSEBUTTONDOWN:
50                if event.button == 1:
51                    p = Particle()
52                    vx = random.uniform(-10, 10)
53                    vy = random.uniform(-10, 0)
54                    init_particle(p, event.pos, (vx, vy))
55                    particle_list.append(p)
56        if should_quit:
57            break
58
59        for p in particle_list:
60            update_particle(p, width, height, dt, gy)
61
62        particle_list[:] = [p for p in particle_list if p.is_alive]
63
64        screen.fill(pygame.Color("black"))
65        for p in particle_list:
66            draw_particle(p, screen)
67        pygame.display.update()
68
69    pygame.quit()
70
71
72if __name__ == "__main__":
73    main()
Lines 9-12 and 52-54

We define a function init_particle to initialize an object p of type Particle, and call it from main.

Note that only p appears in the main side (lines 52-54), and p.x, p.vx, etc. are not directly touched anymore. We pass p as the first argument to init_particle and let the function manipulate p’s attributes.

Lines 15-20 and 60, lines 23-25 and 66
Similarly, we refactor function that updates the state and the function that draws.
Line 62
There is no change here. Here, we directly read the is_alive attribute of p. If we try to refactor a function here, we need to create a function that just returns p.is_alive. Because it does not help improve readability, we simply read the attribute from the main side.
For the deletion of dead particles, I think it would be cleaner if we refactor out the whole right hand side of the line as a function.
Here, we are thinking of refactoring out processings for a single Particle.

It’s a different job to organize the processing of the “list of particles”. We’ll get into this later when we define the AppMain class.

Why do you allow only reading of attributes (and avoid writing whenever possible) when it is difficult to confine the reading and writing of individual attributes to a function?
The reason is the same as for global variables.

It would be nice to avoid both reading and writing, but if that is not possible, at least preventing attributes from being rewritten from the outside will minimize confusion.

5.5. Putting Data and Operations Together

The functions init_particle, move_particle, and draw_particle are defined outside of the class Particle definition. However, since these are functions that deal with the internal attributes of objects of type Particle, it is natural to write them in a way that makes it clear that they are “internal” to the Particle class.

In Python, this is accomplished by writing def method_xyz(): inside class ClassABC:. The result of this definition is a “function” named ClassABC.method_xyz. This is called a method method_xyz belonging to the class ClassABC.

Now we can call this method by writing in a way like:

object_abc.method_xyz(argument1, argument2, argument3)

which is automatically translated to:

ClassABC.method_xyz(object_abc, argument1, argument2, argument3)

where it is assumed that object_abc is an instance of ClassABC.

In other words, the method to be called is determined by the class that the object is an instance of, not by writing the class name directly. Also note that the object itself is automatically passed as the first argument to the method.

This is what the method call in the form of object.method() is, which we have seen many times.

particle.py (ver 7.0)
 1import random
 2import pygame
 3
 4
 5class Particle:
 6    def __init__(self, pos, vel):
 7        self.is_alive = True
 8        self.x, self.y = pos
 9        self.vx, self.vy = vel
10
11    def update(self, width, height, dt, gy):
12        self.vy += gy * dt
13        self.x += self.vx * dt
14        self.y += self.vy * dt
15        if self.x < 0 or self.x > width or self.y > height:
16            self.is_alive = False
17
18    def draw(self, screen):
19        radius = 10
20        pygame.draw.circle(screen, pygame.Color("green"), (self.x, self.y), radius)
21
22
23def main():
24    pygame.init()
25    width, height = 600, 400
26    screen = pygame.display.set_mode((width, height))
27    clock = pygame.time.Clock()
28
29    dt = 1.0
30    gy = 0.5
31    particle_list = []
32
33    while True:
34        frames_per_second = 60
35        clock.tick(frames_per_second)
36
37        should_quit = False
38        for event in pygame.event.get():
39            if event.type == pygame.QUIT:
40                should_quit = True
41            elif event.type == pygame.KEYDOWN:
42                if event.key == pygame.K_ESCAPE:
43                    should_quit = True
44            elif event.type == pygame.MOUSEBUTTONDOWN:
45                if event.button == 1:
46
47                    vx = random.uniform(-10, 10)
48                    vy = random.uniform(-10, 0)
49                    p = Particle(event.pos, (vx, vy))
50                    particle_list.append(p)
51        if should_quit:
52            break
53
54        for p in particle_list:
55            p.update(width, height, dt, gy)
56
57        particle_list[:] = [p for p in particle_list if p.is_alive]
58
59        screen.fill(pygame.Color("black"))
60        for p in particle_list:
61            p.draw(screen)
62        pygame.display.update()
63
64    pygame.quit()
65
66
67if __name__ == "__main__":
68    main()
Lines 6-20

We move the three functions into the class definition and make them methods belonging to Particle. Finally, we can write something meaningful within the class definition instead of pass.

The “_particle” part in the latter half of the function name is no longer necessary, since we know it belongs to the Particle type. Also, it is a Python’s rule that the initialization method should be named __init__.

Note that the first argument of all three methods is now named self. The object itself is passed to this.

The name “self” is actually just a convention, but always use this name.

Lines 46-49

We used to create an object by p = Particle() and initialize it by init_particle(p, arg1, arg2) so far, but now we can simply write p = Particle(arg1, arg2), because Python promises to create the object and then call Particle.__init__(p, arg1, arg2) when Particle(arg1, arg2) is called.

In Python, a method with a name like __xyz__ is called a special method. A special method is not intended to be called directly in the program, but is designed to be called automatically under certain conditions.

Line 55, Line 61
The calls to move and draw have also been rewritten as “method calls”.
I’m getting confused about the relationship between classes and objects (or instances?). Did we just define a class? Or did we define an object?
A class is a description of what attributes and methods an object has. Based on the definition of a class, an object that is an instance of the class can be created.

The relationship between a class and an object that is an instance of the class is the same as that between an integer and its instances such 1 or 2, or that between a cat and its instances such as Alice’s Dinah or Hermione’s Crookshanks.

Defining a class determines how the object, which is an instance of the class, will behave. In this sense, the process of defining a class is also the process of defining an object.

Assuming tha a class A has a method m defined and we create an instance a of A, which is the correct term for m, “method m of A” or “method m of a”?
They are both correct. It just depends on the context, i.e., are we talking about a specific a or the class A in general.
When using classes to organize a program, is it normal to create an external function and then replace it with a method?
No, this is a redundant procedure to help you understand the concept of classes. It is normal to define them as methods from the beginning.

Sometimes you may need to refactor existing code written without classes to use classes, but even in such a case, it is normal to not go in this circuitous way, but to follow the example of creating AppMain that you will see later.

I’m just a passing Java programmer. Normally in Java, all attributes are set not readable or writable from the outside, and methods are provided to read and write them if necessary. I don’t feel comfortable with the design of the is_alive attribute being readable from the outside, as in this example.
Each programming language has its own characteristics, so the recommended way of writing is different; Java should certainly be written that way, and some people prefer to write Python that way.

The design described in the question can be written in Python as follows (with all irrelevant parts omitted):

class Particle:
    def __init__(self):
        self._is_alive = True

    def is_alive(self):
        return self._is_alive

The attribute is_alive in the original code has been renamed to _is_alive, as it is a Python convention that if the name of an attribute begins with _, it should not be read or written by anyone outside the class. (In Java and C++ terms, this is equivalent to specifying it as private)

To read the value of this attribute from outside the class, we use the separately prepared method is_alive. For example, line 57 of the original code should be written as follows:

particle_list[:] = [p for p in particle_list if p.is_alive()]

A method that only reads an attribute is called a getter. Similarly, if a method is provided to set the value of an attribute, it is called a setter. They are sometimes collectively called accessors.

In Java and C++, the use of accessors is almost mandatory because these languages do not provide a property mechanism as described in Chapter 6. Please wait until Chapter 6 for details.

Nevertheless, some people prefer to have getters and setters also in Python. There are many reasons for this, such as a desire to share design with other languages.

5.6. After All, What Did We Do?

We have seen an example of how to organize a program by grouping related data together and providing methods to manipulate them through the mechanism called class. I hope you find the resulting code somewhat easier to read, but the principle behind this may not be clear to many of you yet.

To put it in a motto-like way, we can say that the responsibility to “move according to the laws of motion” has been transferred from the main program to the object. In the original code, the main program instructed the motion of the particles in detail. In contrast, in the rearranged program, the main program only asks the object to “update its state” or “draw itself,” and leaves the details to the object itself.

This style of dividing up the program’s responsibilities and leaving the details to the objects is called object-oriented programming.

I don’t understand what in the world you’re talking about. After all, I am the one doing the programming, right? I don’t know what you mean by transferring responsibilities.
Even if you write all the code by yourself, it’s beneficial because it reduces the number of things you have to take into account at the same time.

When you’re writing the code of the Particle class, you (ideally) don’t have to pay attention to the code using it, and when you’re writing the code to use it, you (ideally) don’t have to worry about the internal implementation the Particle.

Needless to say, this feature is beneficial when there are multiple people working on the same project.

I think the definition of “object-oriented” is somewhat vague. Isn’t writing a program using classes object-oriented?
There is no clear definition of the concept of object orientation.

It is not something that has been strictly defined by anyone in some document, but has been fostered over a long period of time, and a consensus has been slowly formed in the community of computer scientists and programmers.

So if you read some older books, you may find that they explain things in a slightly different way with different emphasis. Even today, different people say slightly different things, but I think most people would agree that the key idea is to divide up the responsibilities and let the objects do the work.

The various features provided in object-oriented programming languages are a means to facilitate the implementation of this philosophy, and the specific features used vary from language to language. Therefore, it is difficult to define object orientation based on the presence or absence of a specific feature.

It would be nice if it were as simple as “This is object-oriented because you use classes” or “This is object-oriented because you use objects,” but it is not that simple. There are object-oriented programming languages that do not have classes, and there are languages that are not object-oriented even though they have the notion of objects in their language specifications (C is one such language, in fact; the “objects” in the C language specification have nothing to do with object orientation).

I’m going to use C as my main language in the future. Is it useless to learn object-oriented programming in this course?
Not at all. Even if a language does not directly support object-oriented programming, you can still use its concepts.

In C, it is possible to use structs and functions to organize code in a way similar to particle.py ver 6.0, and in fact, many people write large programs in this style.

Is object-oriented programming the only methodology for “reducing the number of factors that need to be considered simultaneously”?
There are many others. For example, functional programming is worth learning.

Functional programming is generally characterized by the following points.

  • A function is a pure function in the mathematical sense. In other words, only functions whose return values are uniquely determined when the arguments are determined are considered.
  • Reassignment to variables is not allowed.

In other words, functions and variables are treated in the same way as in ordinary mathematics (assignments such as x = x + 1 may not surprise you since you are used to them, but they are mathematically strange).

This may seem very restrictive, but by using various techniques, you can achieve computations equivalent to other programming languages.

You can clearly see from the above features that functional programming is effective in “reducing the number of factors that need to be considered simultaneously”. The behavior of a function is not affected by anything other than its arguments, and there is no need to think “Has this variable changed its value somewhere?” Because of its high affinity with mathematics, it is also suitable for theoretical analysis and verification.

If you want to build your entire software using functional programming, you should use a language that supports functional programming, but even when you use another programming language, it is possible and beneficial to partially incorporate functional programming concepts (just as C can partially incorporate object-orientation concepts). For example, in Python, the list comprehension notation is a feature with a flavor of functional programming.

5.7. Organizing the Code a Little More

So far, we have defined only the Particle class that represents a particle. In the same way, we will organize other data as well.

5.7.1. World

In the current program, we pass width, height, dt, and gy as arguments to the move method of Particle. In other words, every time the main program asks a particle to move, it tells a particle that the size of the world is width by height pixels, the time step is dt, and the acceleration of gravity is gy.

However, if we want to leave the responsibility of obeying the laws of motion to the object, it is also possible that we tell the object these information when it is created in the world, and then the object can refer to this information at its own risk.

So let’s create a class called World, although this naming may sound like a bit of a stretch, and consider these four variables to be attributes of a World type object.

particle.py (ver 8.0)
 1import random
 2import pygame
 3
 4
 5class World:
 6    def __init__(self, width, height, dt, gy):
 7        self.width = width
 8        self.height = height
 9        self.dt = dt
10        self.gy = gy
11
12
13class Particle:
14    def __init__(self, pos, vel, world):
15        self.is_alive = True
16        self.x, self.y = pos
17        self.vx, self.vy = vel
18        self.world = world
19
20    def update(self):
21        self.vy += self.world.gy * self.world.dt
22        self.x += self.vx * self.world.dt
23        self.y += self.vy * self.world.dt
24        if self.x < 0 or self.x > self.world.width or self.y > self.world.height:
25            self.is_alive = False
26
27    def draw(self, screen):
28        radius = 10
29        pygame.draw.circle(screen, pygame.Color("green"), (self.x, self.y), radius)
30
31
32def main():
33    pygame.init()
34    width, height = 600, 400
35    screen = pygame.display.set_mode((width, height))
36    clock = pygame.time.Clock()
37
38    world = World(width, height, dt=1.0, gy=0.5)
39    particle_list = []
40
41    while True:
42        frames_per_second = 60
43        clock.tick(frames_per_second)
44
45        should_quit = False
46        for event in pygame.event.get():
47            if event.type == pygame.QUIT:
48                should_quit = True
49            elif event.type == pygame.KEYDOWN:
50                if event.key == pygame.K_ESCAPE:
51                    should_quit = True
52            elif event.type == pygame.MOUSEBUTTONDOWN:
53                if event.button == 1:
54                    vx = random.uniform(-10, 10)
55                    vy = random.uniform(-10, 0)
56                    p = Particle(event.pos, (vx, vy), world)
57                    particle_list.append(p)
58        if should_quit:
59            break
60
61        for p in particle_list:
62            p.update()
63
64        particle_list[:] = [p for p in particle_list if p.is_alive]
65
66        screen.fill(pygame.Color("black"))
67        for p in particle_list:
68            p.draw(screen)
69        pygame.display.update()
70
71    pygame.quit()
72
73
74if __name__ == "__main__":
75    main()
Lines 5-10

We define the class World. The only method is __init__, which takes four arguments (in addition to self) and sets them as attributes of itself.

As in the Particle class, the main task of __init__ in general is to initialize the values of the attributes.

Line 38.

At the beginning of the function main, we create an object of type World and put it in the variable world.

Python is case-sensitive. We use World, whose first character is capital, as a class name, and use world, of which all the characters are lowercase, as a variable name.

Lines 14-18 and 56

In the main function, world is passed as an argument when creating a Particle type object. The definition of __init__ of Particle is also modified to keep the passed world as its own attribute.

Note that what this attribute holds is a reference to a World type object. There is only one World object in this simulation world, and each particle owns a “baggage tag” connected to the unique World object, as opposed to that each particle has its own copy of world.

Lines 20-24 and 62
The update method of Particle no longer needs any argument other than self. The values needed for the calculation are read from the attributes of self.world.
The structure is becoming more and more confusing.
You can try visualizing it with the Online Python Tutor.

Unfortunately, you cannot use pygame in the Online Python Tutor, but you can write a simplified version of it. It is enough to understand the structure:

class World:
    def __init__(self, width, height, dt, gy):
        self.width = width
        self.height = height
        self.dt = dt
        self.gy = gy


class Particle:
    def __init__(self, pos, vel, world):
        self.is_alive = True
        self.x, self.y = pos
        self.vx, self.vy = vel
        self.world = world

def main():
    w = World(600, 400, 1, 2)
    p1 = Particle((100, 120), (0, 0), w)
    p2 = Particle((200, 220), (0, 0), w)

main()
When I visualize some code in the Online Python Tutor, I get confused because the “Objects” area on the right has both classes and instances. Is a class an object?
This is indeed a confusing point. It is best not to worry too much about it when you are a beginner.

In Python, not only data but also functions and modules are treated as objects, as I mentioned in one of previous Q&As. Similarly, classes are treated as objects.

The meaning of “treated as an object” may not be clear to you, but a simple understanding such as that you can assign it to a variable, or pass it as an argument to a function or method, is enough. This feature is sometimes useful, and we will use it in later chapters.

In the Online Python Tutor, classes, functions, and modules are all listed in the area of “Objects”, but it’s easier to just focus on the placement of data instances without worrying too much about the others.

If we create such a thing like this World type object and let it shared by all the Particle type objects, isn’t it almost the same as creating a global variable?
Yes, there is that concern. However, there are some advantages of not making it a global variable.

For example, if you want to simulate and compare multiple worlds (such as two environments with different gravitational accelerations), you can create multiple instances of World and visualize the computation of each world in parallel. If height, width, dt, and gy were global variables, this would not be possible.

However, the fact that an instance of World is shared by almost all other objects certainly has the same disadvantages as global variables. Therefore, it is advisable to minimize the number of attributes in World and make it read-only. In fact, that is how we have designed it.

5.7.2. AppMain

Here is an example where the main program side is also provided as a class. Let’s call it AppMain (as in Application Main). We are going to create an object that represents the kinematics simulation application itself, and define its attributes and operations.

particle.py (ver 9.0)
32class AppMain:
33    def run(self):
34        pygame.init()
35        width, height = 600, 400
36        screen = pygame.display.set_mode((width, height))
37        clock = pygame.time.Clock()
38
39        world = World(width, height, dt=1.0, gy=0.5)
40        particle_list = []
41
42        while True:
43            frames_per_second = 60
44            clock.tick(frames_per_second)
45
46            should_quit = False
47            for event in pygame.event.get():
48                if event.type == pygame.QUIT:
49                    should_quit = True
50                elif event.type == pygame.KEYDOWN:
51                    if event.key == pygame.K_ESCAPE:
52                        should_quit = True
53                elif event.type == pygame.MOUSEBUTTONDOWN:
54                    if event.button == 1:
55                        vx = random.uniform(-10, 10)
56                        vy = random.uniform(-10, 0)
57                        p = Particle(event.pos, (vx, vy), world)
58                        particle_list.append(p)
59            if should_quit:
60                break
61
62            for p in particle_list:
63                p.update()
64
65            particle_list[:] = [p for p in particle_list if p.is_alive]
66
67            screen.fill(pygame.Color("black"))
68            for p in particle_list:
69                p.draw(screen)
70            pygame.display.update()
71
72        pygame.quit()
73
74
75if __name__ == "__main__":
76    app = AppMain()
77    app.run()
Lines 32-72

We define the class AppMain, and change the main function to the run method. The contents are just copied.

When you turn a normal function into a method, make sure to add the extra first argument self to the method. (It is easy to forget this when you are not used to it. It is also easy to forget if you are used to other languages)

Lines 76-77
An AppMain type object is created and assigned to the variable app, of which the method run is called immediately.

After confirming that it correctly runs, we can continue to refactor some of the processing in run into methods such as initialization, state update, drawing, etc.

In the case of ordinary functions, we need to add arguments and return values to pass the values of local variables (remember hello_pygame.py when we refactored it into functions). On the other hand, if you want to refactor them into methods, you have another option of making them attributes of self, i.e. of the object itself, and share them between methods.

In our particular case of refactoring update, draw, and add_particle as methods, we need to figure out how to pass particle_list, screen, world, pos, and button between them and run.

Here, we adopt the following design. This is just a design example. (There is no single correct answer).

  • particle_list, screen, and world are attributes of self because they should be kept while this simulation application is running.
  • pos and button are temporary values that are generated when the mouse button is pressed, so they should be passed as arguments.
  • Items that are used only in the run method are left as local variables.

And if we decide to put all the initialization of self’s attributes in __init__, we can organize the code as follows.

particle.py (ver 10.0)
32class AppMain:
33    def __init__(self):
34        pygame.init()
35        width, height = 600, 400
36        self.screen = pygame.display.set_mode((width, height))
37        self.world = World(width, height, dt=1.0, gy=0.5)
38        self.particle_list = []
39
40    def update(self):
41        for p in self.particle_list:
42            p.update()
43        self.particle_list[:] = [p for p in self.particle_list if p.is_alive]
44
45    def draw(self):
46        self.screen.fill(pygame.Color("black"))
47        for p in self.particle_list:
48            p.draw(self.screen)
49        pygame.display.update()
50
51    def add_particle(self, pos, button):
52        if button == 1:
53            vx = random.uniform(-10, 10)
54            vy = random.uniform(-10, 0)
55            p = Particle(pos, (vx, vy), self.world)
56            self.particle_list.append(p)
57
58    def run(self):
59        clock = pygame.time.Clock()
60
61        while True:
62            frames_per_second = 60
63            clock.tick(frames_per_second)
64
65            should_quit = False
66            for event in pygame.event.get():
67                if event.type == pygame.QUIT:
68                    should_quit = True
69                elif event.type == pygame.KEYDOWN:
70                    if event.key == pygame.K_ESCAPE:
71                        should_quit = True
72                elif event.type == pygame.MOUSEBUTTONDOWN:
73                    self.add_particle(event.pos, event.button)
74            if should_quit:
75                break
76
77            self.update()
78            self.draw()
79
80        pygame.quit()
81
82
83if __name__ == "__main__":
84    app = AppMain()
85    app.run()
I can understand the explanation, but I don’t think I can do this kind of separation myself. Do I always have to do this when I write my own programs?
You don’t have to do it all at once.

Here we have refactored out four methods at once (to avoid too much text), but normally you should deal with them one at a time, each time considering how to handle the variables of concern. As you go through this process, you may think “Oh, that variable I just used as an argument should have been an attribute of self,” and may wanto revise it.

This kind of work is essentially “design”, so there is no single right answer. Leaving only one big run method, as in ver 9.0 of particle.py, is not necessarily a mistake. At the same time, ver 10.0 is not necessarily the ideal form. In fact, I think a common design is to separate the event handling part into a separate method or even a separate class. Just try your best as much as you can.

What exactly are the advantages and disadvantages of having variables in AppMain as local variables in run vs. attributes of the AppMain type object?
It is similar to the advantages and disadvantages of using global variables.

When shared between methods as attribute variables, we have advantages such as

  • Simpler code on the caller’s side since there is no need to pass them as arguments.
  • Easier to deal with when the number of items to be passed increases or decreases as the program evolves.

while we have disadvantages such as

  • The number of factors that need to be considered simultaneously increases.
Should we always create such a class like AppMain? I think I can leave the main function and some auxiliary functions as they are.
This is just a design example, so there is no right or wrong answer.
From the examples I’ve seen so far, it seems that a function and a method is often named as “verb” or “verb + noun”. Is there any such rule?
There is no specific rule, but doing like this makes the code often easier to read. There are exceptions, of course.

An example of not using a verb is to use the noun that represents the return value, such as len (which returns length) or math.sqrt (which returns square root) in the standard Python functions.

When the return value is of type bool, naming starting with the third person singular present tense of the verb is also common. Examples include the string type methods isalpha (which returns True if all characters in the string are alphabetic) and startswith (which returns True if the string starts with the string passed as an argument).

A good trick is to name a function or a method so that the code that calls it reads like English, which often leads to good naming. For example, the reason for using the third person present singular is that it is easier to read when used as a condition in an if statement:

if password.isalpha():
    print("Password should include non-alphabet letters.")

5.8. Exercise

The following example is to make Particle have its color and radius as attributes. The __init__ arguments radius and color have default values, so that existing codes using Particle is not affected.

particle.py (ver 11.0)
13class Particle:
14    def __init__(self, pos, vel, world, radius=10.0, color="green"):
15        self.is_alive = True
16        self.x, self.y = pos
17        self.vx, self.vy = vel
18        self.world = world
19        self.radius = radius
20        self.color = pygame.Color(color)
21
22    def update(self):
23        self.vy += self.world.gy * self.world.dt
24        self.x += self.vx * self.world.dt
25        self.y += self.vy * self.world.dt
26        if self.x < 0 or self.x > self.world.width or self.y > self.world.height:
27            self.is_alive = False
28
29    def draw(self, screen):
30        pygame.draw.circle(screen, self.color, (self.x, self.y), self.radius)
31

Once you have made these changes, make sure to commit the changes. In the next chapter, we will resume from this state.

Problem 5-1

Modify AppMain such that the color and radius of particles are also generated randomly.

(You can prepare the list of colors in advance)

Problem 5-2

Add a method add_multiple_particles to AppMain, which creates multiple particles (with different initial velocities) from the current position, so that multiple particles can be thrown with a single click of the right mouse button.

Problem 5-3

We want to make the initial velocity of a particle not random but determined by the user’s action.

Modify AppMain such that the position is remembered when the left mouse button is pressed, and the particle is thrown when the left mouse button is released (MOUSEBUTTONUP event). Determine the initial velocity based on the relative relationship between the current position and the position memorized when the button is pressed so that the user can intuitively throw particles.

(It is also interesting to devise graphics drawing to improve usability)

Problem 5-4

Define a class representing a cuboid solid, and provide methods volume, surface_area, and diagonal that calculate the volume, total surface area, and diagonal length, respectively.

Rewrite the pass part of the following program such that the program can be executed successfully:

import math

class Cuboid:
    pass

shape1 = Cuboid(width=6, height=4, depth=5)
assert shape1.volume() == 120
assert shape1.surface_area() == 148
assert shape1.diagonal() == math.sqrt(77)

shape2 = Cuboid(width=3, height=3, depth=2)
assert shape2.volume() == 18
assert shape2.surface_area() == 42
assert shape2.diagonal() == math.sqrt(22)