Classes, Constructors, and Attributes - Arcade 2023 Documentation
Classes, Constructors, and Attributes - Arcade 2023 Documentation
Without classes, our Python code to store the information might look like this:
1 name = "Link"
2 outfit = "Green"
3 max_hit_points = 50
4 current_hit_points = 50
5 armor_amount = 6
6 max_speed = 10
In order to do anything with this character, we’ll need to pass all that data to a function. With so
many parameters, that function gets complex and hard to manage.
Keeping all these data points organized becomes difficult very quickly. How do we keep a monster’s
hit points separated from the player’s hit points? Because when we add monsters to the game, they’ll
have their own attributes. In fact, just about every item in an adventure game has attributes.
There needs to be a better way. Somehow our program needs to package up all those data fields for
easy management.
Let’s code an example using our adventure character. First, we tell the computer we are defining a
class with the class keyword, and then we give the class a name that starts with a capital letter. Just
like with functions and loops, we end the statement with a colon, and everything associated with the
class will be indented below it:
1 class Character:
Unlike variables, all class names should start with a capital letter. While you can use a lower-case
variable, you never should. Following this pattern of lower-case for variables and upper-case for
classes makes it easy to tell which is which.
1 class Character:
2 """
3 This is a class that represents the player character.
4 """
Yes, the code will run fine without any comments. It is optional. However good documentation is
important to maintainable code, even if you are the only person using the code.
The cool feature about creating comments this way, is the text can be pulled out automatically to
form a website for your API documentation. All the classes and functions in the Arcade library’s API
are created with these comments. You can see the result here:
https://api.arcade.academy/en/latest/arcade.html
For each of those examples, you can click on the “source” link and quickly go to the source code for
that function or class.
Any time we create a new instance of a class, we need code that will create our attributes (variables)
and set them to default values. In Python, this is the __init__ method.
First, any function in a class is called a method, rather than a function. This helps us keep straight
what is in a class, and what isn’t.
Second, the initialization method is a magic method that is called automatically. Yes, Python
programmers actually call methods that are automatically invoked “magic methods.”
Third, to signify a method is magic, Python surrounds the method name with double underscores.
Two underscores in the front, and two underscores in the back. The short-name for double-
underline is dunder, and these magic methods are also known as dunder methods.
1 class Character:
2 """
3 This is a class that represents the player character.
4 """
5 def __init__(self):
6 """ This is a method that sets up the variables in the object. """
The most common mistakes people make when typing this in is to use only one underscore before
and after the init, and to forget that there is a space between def and the first underscore.
All methods in a class have at least one parameter, and the first parameter is always self. We’ll
explain about self in the next section.
Methods follow this rule too, with one exception. The self parameter refers to memory associated
with each instance of the class. We can use that self to create variables that keep their value for as
long as the object exists. We call variables that exist as part of the class either attributes, fields, or
instance variables. The terms mean the same thing. Attributes must be set to a default value. That
value is often 0, an empty string, or the special value None.
1 class Character:
2 """
3 This is a class that represents the player character.
4 """
5 def __init__(self):
6 """ This is a method that sets up the variables in the object. """
7 self.name = ""
8 self.outfit = ""
9 self.max_hit_points = 0
10 self.current_hit_points = 0
11 self.armor_amount = 0
12 self.max_speed = 0
In the example above, if we had failed to put self. in front, the computer would completely forget
about the variables once the __init__ function was done.
Here’s another example, we are defining a class called Address which has attributes for each field of
a US mailing address.
1 class Address:
2 """ Hold all the fields for a mailing address. """
3 def __init__(self):
4 """ Set up the address fields. """
5 self.name = ""
6 self.line1 = ""
7 self.line2 = ""
8 self.city = ""
9 self.state = ""
10 self.zip = ""
In the code above, Address is the class name. The variables in the class are the attributes.
We need a variable that will point to our address. In this case, we’ve called it home_address. We’ll set
that variable equal to the new instance of the class we create. We create an new instance by using
the name of the class (Address), followed by parentheses. This will “magically” call the __init__
method which will set up fields/attributes for the class.
In this case, Address is a class. It defines what an address looks like. The home_address variable
points to an object. An object is an instance of a class. It is the actual address. As another example,
“Human” is a class, while “Samantha” and “Pete” are instances of the class.
You can set the object’s attributes using the dot operator. First, use the variable that points to our
object, immediately follow that with a period, then the attribute name.
1 def main():
2 # Create an address
3 home_address = Address()
4
5 # Set the fields in the address
6 home_address.name = "John Smith"
7 home_address.line1 = "701 N. C Street"
8 home_address.line2 = "Carver Science Building"
9 home_address.city = "Indianola"
10 home_address.state = "IA"
11 home_address.zip = "50125"
A second variable can be created that points to a completely different instance of the Address class:
1 # Create another address
2 vacation_home_address = Address()
3
4 # Set the fields in the address
5 vacation_home_address.name = "John Smith"
6 vacation_home_address.line1 = "1122 Main Street"
7 vacation_home_address.line2 = ""
8 vacation_home_address.city = "Panama City Beach"
9 vacation_home_address.state = "FL"
10 vacation_home_address.zip = "32407"
11
12 print("The client's main home is in " + home_address.city)
13 print("His vacation home is in " + vacation_home_address.city)
Attributes are not limited to being simple strings and numbers! If you have a class that represents a
graph, you can store all the data points in an attribute that is a list. Attributes can even be other
objects. An object that represents a player character in an adventure could have an attribute with
another object that represents a magical hat.
The first common mistake when creating an object is to forget the parentheses:
The terrible thing about this mistake is that the program won’t stop or give you an error. Try
running the example we just created with the two different addresses. Take out the parentheses. The
program runs without error, but both the vacation home and the home address say we are in
Panama City! That’s because without the parentheses we don’t create a new address, we just use the
same block of memory and write the new information over the old, so everything points to the same
address.
Another very common mistake when working with classes is to forget to specify which instance of
the class you want to work with. If only one address is created, it is natural to assume the computer
will know to use that address you are talking about. This is not the case.
This code will run without generating an exception, but it still isn’t correct. Line 15 creates a variable
called name, but it is completely different than the name that is part of Address. So we think we’ve set
the name, but we haven’t.
Line 18 does refer to Address, but not my_address. Frustratingly it runs without alerting us to an
error, but the code isn’t modifying my_address. Instead it sets something called a static variable,
which we’ll talk about later.
Think of it this way. If you are in a room of people, saying “Age is 18” is confusing. Saying “Human’s
age is 18” is also confusing. Saying “Sally’s age is 18” is ideal, because you are saying which instance
of human you are referring to. You have to do this with programming, even if there is only one
human in the room.
Another mistake is on line 22. That line also runs fine, but it creates a new attribute called naem
instead of setting the desired attribute name.
16.4. Using Objects in Functions
Putting lots of data fields into a class makes it easy to pass data in and out of a function. In this
example, the function takes in an address as a parameter and prints it out on the screen. It is not
necessary to pass parameters for each field of the address.
1 def print_address(address):
2 """ Print an address to the screen """
3
4 print(address.name)
5 # If there is a line1 in the address, print it
6 if len(address.line1) > 0:
7 print(address.line1)
8 # If there is a line2 in the address, print it
9 if len(address.line2) > 0:
10 print( address.line2 )
11 print(address.city + ", " + address.state + " " + address.zip)
12
13
14 def main():
15 # ... code for creating home_address and vacation_home_address
16 # goes here.
17 print_address(home_address)
18 print()
19 print_address(vacation_home_address)
20
21
22 main()
We can modify the code in our constructor to keep this from happening. First, let’s add a print
statement to our __init__ just to demonstrate that it is really being called.
1 class Dog():
2 def __init__(self):
3 """ Constructor """
4 self.name = ""
5 print("A new dog is born!")
6
7
8 def main():
9 # This creates the dog
10 my_dog = Dog()
11 print(f"The dog's name is: {my_dog.name}")
When a Dog object is created on line 10, the __init__ function is “magically” called and the message
is printed to the screen.
We can add a parameter to our constructor, so that it requires us to pass in a name for the dog. Try
running this code.
1 class Dog():
2 def __init__(self, new_name):
3 """ Constructor """
4 self.name = new_name
5 print("A new dog is born!")
6
7
8 def main():
9 # This creates the dog
10 my_dog = Dog()
11 print(f"The dog's name is: {my_dog.name}")
12
13
14 main()
The computer is saying it is missing a value for the new_name parameter. It won’t let the dog be
created without a name. We can fix that up by adding a name when we create the dog.
1 class Dog():
2 def __init__(self, new_name):
3 """ Constructor """
4 self.name = new_name
5 print("A new dog is born!")
6
7
8 def main():
9 # This creates the dog
10 my_dog = Dog("Fluffy")
Notice in line 4 we take the value that was passed in as a parameter and assign self.name to have
that same value. Without this line, the dog’s name won’t get set.
As programmers sometimes get tired of making up variable names, it is completely normal to see
code like this:
1 class Dog():
2 def __init__(self, name):
3 """ Constructor """
4 self.name = name
5 print("A new dog is born!")
6
7
8 def main():
9 # This creates the dog
10 my_dog = Dog("Fluffy")
11
12
13 main()
Though it may seem strange at first, we have two variables at work, not one. The first variable is
name, and that variable is assigned as a parameter when we call the Dog constructor. It goes away as
soon as the Dog constructor is done, and is forgotten about. The second variable is self.name, and
that variable is complete different than name. Its value will stay after the constructor is done.
1 class Address:
2 def __init__(self, line1, line2, city, state, zip, country):
3 self.line1 = line1
4 self.line2 = line2
5 self.city = city
6 self.state = state
7 self.zip = zip
8 self.country = country
9
10
11 def main():
12 # This creates the address
13 my_address = Address("701 N. C Street",
14 "Carver Science Building",
15 "Indianola",
16 "IA",
17 "50125",
18 "USA")
19
20
21 main()
16.7. Typing Attributes
It is possible to tell Python what type of data should be stored in a class attribute. This allows a
programmer to use a tool like mypy and catch errors earlier in the development process.
In this example, we are adding a type definition to the name attribute on line 3. We do this by
following the variable name with a colon, and adding str which is the abbreviation for the string
data type.
1 class Person:
2 def __init__(self):
3 self.name: str = "A"
4
5
6 mary = Person()
7 mary.name = 22
By assigning a number to the name attribute on line 7, we are storing the wrong kind of data. The
program runs, but if we use the mypy tool, it will give us an error saying we’ve made a mistake:
Typing is great for large programs, and for programs where we want to make sure to catch all the
errors we can before shipping to customers.
As we are just learning programming, it can be distracting to try adding typing to our programs at
this stage. But we will be both looking and using, other people’s code which does use typing.
Therefore it is important to know what typing is, even if we don’t need to use it ourselves until later.
This code is repetitive, as we state the fields twice. If your __init__ method is only going to take in
data fields and assign attribute values, you can simplify your code by using a dataclass.
Starting with Python 3.8, you can write the same thing using only this code:
1 @dataclass
2 class Address:
3 name: str = ""
4 line1: str = ""
5 line2: str = ""
6 city: str = ""
7 state: str = ""
8 zip_code: str = ""
This makes the code a lot easier to both write, and to read.
In a few rare cases, we want to share data between all instances of a class. In
this example with a Cat class, we have a population variable. This variable is
not different for each cat.
1 class Cat:
2 population = 0
3
4 def __init__(self, name):
5 self.name = name
6 Cat.population += 1
7
8 def main():
9 cat1 = Cat("Pat")
10 cat2 = Cat("Pepper")
11 cat3 = Cat("Pouncy")
12
13 print("The cat population is:", Cat.population)
14
15 main()
In this case we use Cat.population to keep track of our cat population, and the program will print
out the correct count of 3.
Variables that don’t change for each instance of a class, are called class variables or static
variables. The terms mean the same thing and can be used interchangeably.
You refer to a static variable by using the class name Cat rather than any of the instance names like
cat1.
Static variables aren’t used that often. The only reason we are introducing them here is that it is not
unusual for students to accidentally use a static variable instead of an instance variable. In fact,
Python makes it a bit too easy to ‘blend’ the two concepts together.
For example, we can also print a static variable not just by using the class name, but also by using
the instance name:
When we are reading code and come across a variable like Cat.population, we immediately know it
is static. How? All class names start with a capital letter, so Cat is a class. The only attributes that we
can refer to with a class, rather than an instance, are static variables. So population must be static. If
we use cat1.population, a programmer reading that code might mistakenly assume it is an instance
variable rather than a static variable, so that makes debugging really hard. To reduce confusion,
always refer to static variables using the class name.
In this example, I set population to 4, and each print statement says population is 4. This is confusing
because I set one variable and the others change. If I just use Cat.population to refer to the
population, then I remove that confusion.
1 Cat.population = 4
2 print("The cat population is:", Cat.population)
3 print("The cat population is:", cat2.population)
4 print("The cat population is:", cat1.population)
Here’s where it gets really wild. As we just saw, I can print a static variable by referring to it with an
instance, rather than by the class name. I shouldn’t, but I can.
1 Cat.population = 4
2 cat3.population = 5
3 print("The cat population is:", Cat.population)
4 print("The cat population is:", cat1.population)
5 print("The cat population is:", cat2.population)
6 print("The cat population is:", cat3.population)
In this case Cat.population, cat1.population, and cat2.population all refer to the same static
variable. But once I assign a value to cat3.population it creates a brand-new instance variable. So
all the other cats use the static population value, while cat3 uses a new instance variable with the
same exact name as the static variable. The static variable is shadowed by the instance variable.
Therefore when we print cat3.population we get a 5. That type of bug is very hard to find.
For our purposes, we won’t need to use static variables, we only introduce them so that you can
better understand some confusing errors people occasionally run into.
16.10. Review
In this chapter we learned how to bundle together several related data items into a class. We call
these class attributes, instance variables, or fields. Each instance of a class is an object. Functions
defined in a class are called methods. A special magic method called when an object is created is
the __init__ method, which is used to set up instance variables and assign them their initial values.
Inside the class we refer to instance variables by putting self. in front of them, such as self.name.
Outside the class, we need to use a variable that refers to the class, such as customer.name.
Using classes helps simplify our code. We can use classes to represent:
Characters in a video game, with attributes for health, speed, and armor.
Graphs, with attributes for heading, size, and data.
A customer order, with a list as an attribute for each item in the order.
Data classes can be used to make it easier to define a class with a lot of attributes. Typing can be
used to make sure we don’t put the wrong type of data in an attribute. Static variables are
attributes that don’t change from object to object.
In Lab 6: Text Adventure, you’ll use a class to represent a room in an text adventure. You’ll use
attributes to store the room description, and which rooms are north, south, east and west of it. You’ll
use a list to store all the rooms in your adventure.