Home | GitHub | Pygame Docs
Python is an object oriented programming language, which means that everything in Python is an object. But that doesn't tell us what objects are. The word "object" here just means a collection of data and/or functionality. More specifically, any piece of data attached to a class is called an "attribute". Attributes can be either variables or functions, but attributes that are functions have another common name: methods. Variable attributes can be called "instance variables" to distinguish them from methods, but when talking about classes with other Python programmers, it is generally accepted that "attribute" usually means a variable and "method" means a function. Now, why do they matter and how do they relate to classes? We'll get to that.
You should be familiar with the built-in datatypes such as:
int
float
str
list
tuple
dict
A "class" is ultimately just a container for some data and functionality. The key part to recognize about a "class"
is that the "class" term usually specifically refers to the abstract container itself. So int
is a class
but 1
is not. 1
is what's called an instance of the int
class. Note, however,
int
behaves a bit differently than most classes you'll define for
your own use. But, you can explicitly instantiate int
in a similar fashion to the way you'll be instantiating
your own classes. Just append ()
to the end of the class name to create an instance (so like
int()
would return an int
instance. You can also put any number into the parentheses and it
will create an instance based on that number, otherwise it defaults to 0).
A "type" is pretty much another word for "class" in Python. The type
function returns the class that the
argument is an instance of (if you print type(1)
in Python, you'll get int
). But wait, why did
I say "pretty much another word for 'class'" and not "another word for class?" Well, that part is complicated and well
beyond the scope of this guide. There is a link at the bottom of the page that goes into depth about the relationship
between the two concepts in Python. I only mention that there is a difference for the pedants that might take issue with
my simplification. So, you can ignore all but the first couple of sentences in this paragraph in 99.99999% of cases.
An "object" is an instance of a class. 1
is an object. Here's how to think about class vs instance: A class is
a template for an instance. The class tells you how to construct whatever data structure you're trying to construct, and an
instance is what you get when you follow the template. Again for the pedants: technically everything in python
is an "object" too, but I'm still simplifying here.
Now's the time that we can start actually making classes. So, let's start off by creating a very simple one. Here's a class definition that we will
parse:
1
2
3
class Foo:
def __init__(self):
print('bar')
Let's talk about the class Foo:
part first. This is where we create a class named "Foo". That's it.
No more black magic here. We are literally just telling Python that we want to create a class called "Foo".
Now for the next line, def __init__(self):
. This is called the class constructor. Almost every class you'll ever write
has this part. I'm leaving out a few details here since they are way out of scope of this discussion. This particular function is called the constructor
because it, in essence, "constructs" the class. Generally, you don't want to put anything computationally expensive in the constructor nor do you want to
call a computationally expensive function from the constructor. Its job is to just set up the instance in such a way that you can use it. The
self
parameter is always needed and will get passed in automatically when you instantiate the class. To create an instance of the
above class, you essentially "call" it like you do a function (i.e. add the parentheses): foo_instance = Foo()
. Most classes don't provide
much functionality just by existing. You need an instance of the class. There are exceptions and we'll touch on
those, so let's continue.
Now let's start building a class that we will continue to build on throughout this tutorial:
1
2
3
class Actor:
def __init__(self):
pass
1
2
3
4
5
6
class Actor:
def __init__(self):
self.health = 100
self.attack = 5
self.defense = 0
self.inventory = ['sword', 'shield']
Actor
instance, the constructor is going to assign the values health, attack, defense,
and some inventory to something called self
. The self
parameter represents the class instance (or in other words, it's
referring to itself), so you're really just assigning those bits of data to the instance of the class. This serves a quite useful purpose.
You see, if I didn't prefix all of those with self
, then when the constructor finished running the values would get garbage collected
away and would not exist anymore. That's not what we want at all. So, we assign them to be instance attributes as above and those values stay in
scope for as long as the instance itself exists. Another benefit is that we can change them for individual instances and the changes
only apply to that single instance and no others.
Here's an example:
We can see that all of the attributes we prefixed with self
printed just fine, but as soon as we tried to print the value that wasn't prefixed,
we got an AttributeError
which means that the attribute we tried to access does not exist.
A general rule of thumb for whether something should be an attribute or not is this:
Make it an attribute if you need it later and ONLY if you need it later
Python variable and method names also have a few naming conventions (note that these are common conventions and are not enforced at all in Python). To read the full convention, see PEP 8:
These are the simplest to understand. They're quite simply things like above: self.health = 100
. There are no underscores at the beginning of
the attribute name. The purpose of these attributes is that they are intended to be directly accessed and modified by the user.
These are a little more complicated to understand. A single leading underscore attribute would look like self._health = 100
.
The purpose of these attributes is that they are intended to be "protected". The user can still access these, but the leading underscore is the best
indication to them that tinkering with this variable could break something because it's not intended to be modified directly. These attributes will
be easily accessible under the same name in any subclasses (we will get to those, so be patient).
These are the trickiest to understand. One of these attributes would look like
self.__health = 100
. These attributes are intended to be private; users are not supposed to access these nor are any subclasses.
This doesn't make them inaccessible. They're actually harder to access. This is due to name mangling. When you define one of these
attributes on a class, the name of the attribute is changed outside of the class itself. If my class name is Actor
with the
attribute I listed above, I'd have to access the attribute like instance._Actor__health
, but within the Actor
class,
I could just call self.__health
. We'll talk about the subclass implications after we talk about subclasses in the Inheritance section.
A common thing that beginners do is to name their attributes something like type
or id
. While this usually won't get
you into trouble and it will usually work fine, this is still frowned upon because type
and id
have specific meanings in Python.
The commonly accepted way to use names like these is to append an underscore to the end of the variable name, so instead of self.id
,
you should do self.id_
. There is really no other purpose to a single trailing underscore in an attribute name, it's just a common way to
prevent yourself from accidentally shadowing something else.
Sometimes you want to create multiple classes that have the same basic functionality, but they each behave a bit differently. An example relevant to pygame might be that you have NPCs, Enemies, and the Player. Each one is gonna have pretty much the same basic logic:
Let's say I want to create NPC and Enemy classes. I might start off with defining an Actor class that doesn't have much functionality, but will serve
as the base of my NPC and Enemy classes. That Actor class might be defined in this way:
1
2
3
4
5
class Actor:
def __init__(self, position, image, size):
self.image = image
self.size = size
self.pos = position
position
, image
, and
size
. These are bits of information that I would expect pretty much any character in the game to need.
Now, I want to create an NPC class. I could rewrite all of that code in the constructor of
my NPC class, but that's a lot of overhead as mentioned before. Instead, let's subclass Actor
:
1
2
3
class NPC(Actor):
def __init__(self, position, image, size, dialogue):
self.dialogue = dialogue
Python has no idea what our intentions are. It could assume that we want the constructor of Actor
to be
called immediately, but that's a lot of overhead and might not actually be what we want. So, we have to explicitly
call the constructor of Actor
(side note: Actor here is also known as the superclass of NPC, and that'll
be important in a second). There are a couple of ways to do that: explicitly calling the constructor of Actor
,
or by calling the constructor of the superclass.
Calling Actor's constructor:
1
2
3
4
class NPC(Actor):
def __init__(self, position, image, size, dialogue):
Actor.__init__(self, position, image, size)
self.dialogue = dialogue
1
2
3
4
class NPC(Actor):
def __init__(self, position, image, size, dialogue):
super().__init__(position, image, size)
self.dialogue = dialogue
These look practically identical (and in fact, they practically do the exact same thing in simple cases), but there are a
few reasons to choose one over the other. Generally, super().__init__
is preferred because it's easy to swap
out or rename the superclass if you need to, as well as not needing to remember the self
parameter. And for
longer class names, it's also shorter. But, the situation where you would possibly want to use Actor.__init__
would be in the case of more complicated inheritance structures (long chains of inheritance or multiple inheritance,
both of which are well outside the scope of this article and game design in general, but a link is at the bottom of the
page for an excellent video on the topic from the
mCoding YouTube channel).
The power of class inheritance is that attributes are inherited from the superclass to the subclass. So, an instance of
NPC
as given above has the same attributes as Actor
(note that this would not be the case if we had
not called the constructor of the superclass). Here's an example to play around with:
But remember from the beginning what I said about attributes and methods? I said that methods are attributes, so
anything that applies to variable attributes also applies to methods! This means that methods are inherited too. Try it
in the embed above. Define a method of the Actor
class and try to call it from the instance of NPC
.
One example of that might look like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Actor:
def __init__(self, position, image, size):
self.image = image
self.size = size
self.pos = position
def talk(self, text):
print(text)
class NPC(Actor):
def __init__(self, position, image, size, dialogue):
super().__init__(position, image, size)
self.dialogue = dialogue
npc = NPC("Tom", "assets/tom.png")
print(npc.name)
print(npc.image)
npc.talk("Hello World!")
But there's more that we can do! super().__init__()
in the constructor of a subclass hints that we can access
the methods of the superclass in the subclass! In fact, this is one of the most powerful aspects of inheritance! Let's say
my Actor
superclass has a method func
and I want to run that method in my NPC
subclass
but I also want it to do some additional stuff. One option would be to copy/paste the code from Actor.func
into
NPC.func
. But there's an alternative that is easier to maintain and reduces the amount of duplicate code: call
super().func()
in NPC.func
, like so:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Actor:
def __init__(self, position, image, size):
self.image = image
self.size = size
self.pos = position
def talk(self, text):
print(text)
class NPC(Actor):
def __init__(self, position, image, size, dialogue):
super().__init__(position, image, size)
self.dialogue = dialogue
def talk(self, player_text, npc_text):
print(f"You said {player_text}")
super().talk(npc_text)
npc = NPC((100, 200), "assets/npc.png", (20, 20), "Hello World!")
print(npc.pos)
print(npc.dialogue)
npc.talk("Hello", "Hello there")
Enter some text and code here
Enter some text and code here
Enter some text and code here
Enter some text and code here
Enter some text and code here
Enter some text and code here